1

Тема: AHK: AppiumDriver Class. Android/IOS automation.

Всем добра!
Обёртка над REST API HTTP-сервера 'Appium'.
По заявлению официального источника, может быть выполнена автоматизация приложений, запускаемых на операционных системах "IOS", "Android" и "Windows". Винду, понятное дело, грешно автоматизировать через "посредников", когда есть AHK, а вот с другими, AHK, такой лёгкий и удобный - не работает.

Эта поделка призвана снять бОльшую часть ограничений, но не всю. В частности, этот код будет бесполезен для автоматизации игр и им подобных приложений(возможно, не полностью, требует уточнения). В остальном, всё, что касается взаимодействия с пользовательским интерфейсом, работает на ура, а именно:
+ тыкать кнопки
+ чекать боксы
+ редактировать текст(только редактируемые поля)
+ получать текст(только видимый)
+ скроллить влево/вправо, вверх/вниз
+ эмулировать нажатия пальцем и перетаскивания
# А так же:
++ получать/изменять геолокацию
++ устанавливать/удалять приложения
++ обмен файлами
++ другие плюшки

Под спойлером гифка с примером работы тестового образца, код которого будет в конце.

+ открыть спойлер

https://c.radikal.ru/c26/1902/ea/2fa07c480ff3.gif

Требует подключения к проекту класса для обработки JSON. Большая благодарность teadrinker!
Тестировалось на AHK v1.1.30.01 Unicode x64 под управлением Windows 7 x64.
В тесте принимало участие виртуальное устройство с установленной ОС Android Lollipop(5.1.1 - API Level 22), но должно работать со всеми устройствами которые видит ADB(Android Debug Bridge), начиная с API Level >= 18. Список доступных для подключения девайсов можно узнать в консоли:

adb devices

Манипуляции с девайсом похожи на работу с Selenium. Имеющие опыт подобной автоматизации будут "как рыба в воде".

Из-за отсутствия необходимости, "Яблоки" не тестировал. Если какой-либо метод не работает, ищите ответы в  официальной документации по API Appium. Возможно есть ограничения по типу автоматизации сессии, поддерживающие разные платформы, если, конечно, сам метод не просто описан, но ещё и добавлен. Например, почти всё API для WEB-контекста имеет описание, но ещё не добавлено, если верить документации. Правда, это и не должно быть помехой, так как реализация для NATIVE_APP прекрасно справляется с его автоматизацией.

Так же, следует принять во внимание общее известное ограничение, проявляющееся в том, что всё взаимодействие с пользовательским интерфейсом возможно только в границах ViewPort. Это означает, что только то, что девайс "рендерит", контекст может считать за "существующие" элементы и может с ними взаимодействовать. Например, приложение демонстрирует список из десятка(сотни, тысячи...) элеменов, который прокручивается. Соответственно, как для реального пользователя, так и для обращения к контексту программно, элементы находящиеся вне границ дисплея - не доступны.

И ещё, "из коробки", этот код работать не будет. Один из способов это преодолеть в этом туториале. В интернетах есть и другие решения, но по большей части они все похожи. Тема популярна.
"Дорогу осилит идущий". (с)

Для запуска теста потребуется apk-файл, найти который можно на странице уже давно и благополучно не развивающегося проекта, потому как вся ветвь развития в этом направлении перешла в Appium. Синяя кнопка внизу с текстом "Download apk »". Его нужно положить в дирректорию тестового сценария.


;	++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
;	Обертка над REST API, HTTP-сервера 'Appium'. Детали на официальном источнике:
;	 | http://appium.io/
;	
;	Код написан и опубликован за авторством KusochekDobra, 22.02.2019.
;	Версия 1.0.0
;	
;	Распространяется по лицензии MIT.
;	 | https://ru.wikipedia.org/wiki/%D0%9B%D0%B8%D1%86%D0%B5%D0%BD%D0%B7%D0%B8%D1%8F_MIT
;	
;	Копируйте, изменяйте, распространяйте, продавайте... Но, с обязательным указанием 
;		на источник оригинала.
;	++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
;
;	♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥
;						★★★★★★★★ Благодарности ★★★★★★★★
;	♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥
;	Большая благодарность сообществу "Серого форума" за, всегда живой диалог, интересные
;		идеи и традиционное постоянство.
;	
;	А так же, низкий поклон отцам начинателям, положившим начало AutoHotKey и всем, кто
;		развивает его популярность бескорыстно делясь своим мнением, проектами, временем.
;	♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥
;
;	========================================================================================
;						| ######## |  Полезные ресурсы  | ######## |
;	========================================================================================
;	ADB command list
;	 | https://developer.android.com/studio/command-line/adb#issuingcommands
;	
;	JsonWireProtocol(JSONWP)
;	 | https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
;	
;	WebDriver
;	 | https://w3c.github.io/webdriver/
;	
;	Appium API Documentation
;	 | http://appium.io/docs/en/about-appium/api/
;	
;	Appium Desired Capabilities
;	 | http://appium.io/docs/en/writing-running-appium/caps/
;	
;	XPath
;	 | https://www.w3.org/TR/1999/REC-xpath-19991116/
;	 | https://ru.wikipedia.org/wiki/XPath
;
;	UiSelector
;	 | https://developer.android.com/reference/android/support/test/uiautomator/UiSelector
;	UiScrollable
;	 | https://developer.android.com/reference/android/support/test/uiautomator/UiScrollable
;	========================================================================================
;
;	****************************************************************************************
;	desiredCapabilities	- JSON-сериализованный объект, содержащий желаемые возможности
;							сессии. Ссылка выше.
;						{"desiredCapabilities":{"app":"C:/app/full/path/app.apk", ... }}
;
;	port				- порт, который будет прослушивать Appium в ожидании команд.
;						Стандартный порт 4723, будет установлен по умолчанию, если 
;							appiumDesktop = true.
;							
;	appiumDesktop		- сообщает, будет ли использована Desktop-версия Appium. 
;						Необходимо из-за специфичности подключения, а так же некоторой
;							разницы принимаемых аргументов, некоторых методов.
;	****************************************************************************************

Class AppiumDriver Extends __SearchContext
{
	__New(desiredCapabilities := "", port := "", appiumDesktop := true) {
		if (this._ad := appiumDesktop) {
			if (!WinExist("ahk_exe Appium.exe"))
				throw {"msg":"Для Appium-Desktop - нет запущенного 'Appium.exe'. Получение сессий, или создание подключения не имеет смысла.","error":1000}
			port := !port ? 4723 : port
		} else if !(port) {
			if (inst := this.FindInstances()) {
				if (inst.Length() > 1) {
					str := ""
					For k, v in inst
						str .= v.port " "
					InputBox, newPort, Выберите порт,Оставьте в этом поле только нужный порт для подключения. Отмена завершит приложение.,,,150,,,,,%str%
					if (ErrorLevel)
						ExitApp
					port := Trim(newPort)
				} else
					port := inst[1].port
			} else
				throw {"msg":"Для Appium-CMD, нет запущенных серверов. Для создания нового подключения, запустите сервер и передайте конструктору 'desiredCapabilities'.","error":1000}
		} this._API := Format("http://127.0.0.1:{}/wd/hub/", port)
		
		if (desiredCapabilities) {
			Try
				this._sessionObj := this._NewSession(desiredCapabilities)
			Catch e {
				if (e.error == 2000)
					throw {"msg":"Сервер не запущен, или слушает другой порт. Запустите сервер, и/или выполните соединение на ожидаемом порту.","error":1001}
				throw {"msg":Format("Неизвестная ошибка:`n`n'{}'",e),"error":1}
			}
			this._sessionID := this._sessionObj.sessionID
		} else {
			Try
				this._sessionObj := this.GetSessionList()
			Catch e {
				if (e.error == 2000)
					throw {"msg":"Сервер не активен, или слушает другой порт. Активируйте сервер, и/или выполните соединение на ожидаемом порту.","error":1002}
				throw {"msg":Format("Неизвестная ошибка:`n`n'{}'",e),"error":1}
			}
			
			if (sessionNumber := len := this._sessionObj.Length()) {
				While (len > 1) {
					InputBox,sessionNumber,Выбор подключения,Всего активных сеансов = '%len%'. Укажите желаемый номер сессии для подключения. Отмена завершит приложение.,,,150,,,,,1
					if (ErrorLevel)
						ExitApp
					if sessionNumber is not integer
					{
						MsgBox,16,Ошибка!,Значение '%sessionNumber%' не является целым числом. Укажите значение от 1 до %len% включительно.
						Continue
					} if (sessionNumber < 1 || sessionNumber > len) {
						MsgBox,16,Ошибка!,Значение '%sessionNumber%' - вне диапазона. Укажите значение от 1 до %len% включительно.
						Continue
					} Break
				} this._sessionID := this._sessionObj[sessionNumber].id
			} else
				throw {"msg":Format("На порту '{}' - активных сессий не найдено. Для создания новой передайте конструктору 'desiredCapabilities', или выберите другой порт.", port),"error":1003}
		} this.action := new this.TouchActions(this._API, this._sessionID, this._ad)
	}
	
	/*	
	*	Возвращает массив объектов с параметрами запущенных серверов из командной строки,
	*		или false - если таковых не найдено
	*	[
	*		{
	*			port:		"порт прослушиваемый сервером",
	*			cmdLine:	"аргументы cmd",
	*			h:			"pid сервера"
	*		}, { ... }
	*	]
	*/
	FindInstances() {
		out := []
		For item in ComObjGet("winmgmts:")
			.ExecQuery("SELECT CommandLine FROM Win32_Process WHERE Name = 'cmd.exe'")
			if RegExMatch(item.CommandLine, "--address 127.0.0.1 --port (\d+)", m)
				out.Push({"port": m1, "cmdLine": item.CommandLine, "h": item.Handle})
		Return out.Length() ? out : false
	}
	
	/*	
	*	Создаёт сессию, возвращая объект, идентичный результату вызова GetSessionCapabilities()
	*/
	_NewSession(capabilities) {
		if ((o := this._Post(this._API "session/", capabilities)).error) {
			if (InStr(o.error.Message, "0x80072EFD"))
				throw {"msg":"Не удается установить соединение с сервером","error":2000}
			throw {"msg":"_NewSession()", "error":o.error, "request":o.req}
		} Return o
	}
	
	/*	
	*	Возвращает true, если appPackage установлено.
	*/
	IsAppInstalled(appPackage) {
		temp = {"bundleId":"{}"}
		url := Format("{}session/{}/appium/device/app_installed",this._API,this._sessionID)
		if ((o := this._Post(url, Format(temp, appPackage))).error)
			throw {"msg":Format("IsAppInstalled('{}')", appPackage), "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Устанавливает приложение из полного пути в appPath.
	*	Возвращает: "null"
	*/
	InstallApp(appPath) {
		temp = {"appPath":"{}"}
		url := Format("{}session/{}/appium/device/install_app",this._API,this._sessionID)
		if ((o := this._Post(url, Format(temp, appPath))).error)
			throw {"msg":Format("InstallApp('{}')", appPath), "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Удаляет приложение указанное в appPackage.
	*	Возвращает: "null"
	*/
	RemoveApp(appPackage) {
		temp = {"bundleId":"{}"}
		url := Format("{}session/{}/appium/device/remove_app",this._API,this._sessionID)
		if ((o := this._Post(url, Format(temp, appPackage))).error)
			throw {"msg":Format("RemoveApp('{}')", appPackage), "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Запускает приложение текущей сессии, указанное в capabilities в поле app.
	*	Возвращает: "null"
	*/
	LaunchApp() {
		url := Format("{}session/{}/appium/app/launch",this._API,this._sessionID)
		if ((o := this._Post( url )).error)
			throw {"msg":"LaunchApp()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Завершает приложение текущей сессии, указанное в capabilities в поле app.
	*	Возвращает: "null"
	*/
	CloseApp() {
		url := Format("{}session/{}/appium/app/close",this._API,this._sessionID)
		if ((o := this._Post( url )).error)
			throw {"msg":"CloseApp()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Сбрасывает приложение текущей сессии, указанное в capabilities в поле app.
	*		После перезапуска, состояние приложения будет таким, как если бы его
	*		запустили впервые.
	*	Возвращает: "null"
	*/
	ResetApp() {
		url := Format("{}session/{}/appium/app/reset",this._API,this._sessionID)
		if ((o := this._Post( url )).error)
			throw {"msg":"ResetApp()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Отправляет команду "вернуться". Возвращает объект, "value" которого содержит "true",
	*		если операция проведена успешно
	*/
	Back() {
		if ((o := this._Post(Format("{}session/{}/back",this._API, this._sessionID))).error)
			throw {"msg":"Back()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Блокирует девайс.
	*	seconds	- как долго находиться в состоянии блокировки( !!! ТОЛЬКО ДЛЯ IOS !!! )
	*/
	LockDevice(seconds := "") {
		temp := seconds ? Format("{""seconds"":{1}}", seconds) : ""
		url := Format("{}session/{}/appium/device/lock",this._API,this._sessionID)
		if ((o := this._Post(url, temp)).error)
			throw {"msg":"LockDevice()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	
	*	Разблокирует девайс.
	*/
	UnlockDevice() {
		url := Format("{}session/{}/appium/device/unlock",this._API,this._sessionID)
		if ((o := this._Post( url )).error)
			throw {"msg":"UnlockDevice()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	
	*	Возвращает true, если девайс заблокирован, иначе, false.
	*/
	IsLocked() {
		url := Format("{}session/{}/appium/device/is_locked",this._API,this._sessionID)
		if ((o := this._Post( url )).error)
			throw {"msg":"IsLocked()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Встряхивает девайс( !!! ТОЛЬКО ДЛЯ IOS !!! )
	*		| http://appium.io/docs/en/commands/device/interactions/shake/index.html
	*/
	Shake() {
		url := Format("{}session/{}/appium/device/shake",this._API,this._sessionID)
		if ((o := this._Post( url )).error)
			throw {"msg":"Shake()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Изменяет состояние питания эмулятора(ВКЛ / ВЫКЛ).
	*	state	- может быть только "ON" или "OFF"
	*/
	SetPowerAC(state) {
		temp := Format("{""state"":""{:L}""}", state)
		url := Format("{}session/{}/appium/device/power_ac",this._API,this._sessionID)
		if ((o := this._Post(url, temp)).error)
			throw {"msg":"SetPowerAC()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Изменяет состояние заряда батареи( !!! ТОЛЬКО ДЛЯ Andriod !!! )
	*	percent	- целочисленное значение процентов в интервале [0 - 100]
	*/
	SetPowerCapacity(percent) {
		percent := percent ? percent > 0 ? percent > 100 ? 100 : percent : 0 : 100
		temp := Format("{""percent"":{1}}", percent)
		url := Format("{}session/{}/appium/device/power_capacity",this._API,this._sessionID)
		if ((o := this._Post(url, temp)).error)
			throw {"msg":"SetPowerCapacity()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Возвращает объект, описывающий состояние сервера, "status" которого сообщает
	*		о возможности сервера создавать новые сеансы(doc).
	*			| http://appium.io/docs/en/commands/status/index.html
	*	Всегда возвращает один и тот же результат:
	*	{"status":0,"value":{"build":{"version":"1.10.0"}},"sessionId":null}
	*/
	GetStatus() {
		if ((o := this._Get(this._API "status")).error)
			throw {"msg":"GetStatus()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	
	*	Возвращает имя текущей activity.
	*/
	GetCurrentActivity() {
		url := Format("{}session/{}/appium/device/current_activity",this._API,this._sessionID)
		if ((o := this._Get( url )).error)
			throw {"msg":"GetCurrentActivity()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	
	*	Возвращает LANDSCAPE или PORTRAIT.
	*/
	GetOrientation() {
		url := Format("{}session/{}/orientation",this._API,this._sessionID)
		if ((o := this._Get( url )).error)
			throw {"msg":"GetOrientation()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Принимает регистро-независимую строку = LANDSCAPE или PORTRAIT.
	*	Возвращает: 'Rotation (PORTRAIT) successful.'
	*/
	SetOrientation(orientation := "PORTRAIT") {
		temp = {"orientation":"{}"}
		url := Format("{}session/{}/orientation",this._API,this._sessionID)
		if ((o := this._Post(url, Format(temp, orientation))).error)
			throw {"msg":"SetOrientation()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	
	*	Возвращает имя текущего package.
	*/
	GetCurrentPackage() {
		url := Format("{}session/{}/appium/device/current_package",this._API,this._sessionID)
		if ((o := this._Get( url )).error)
			throw {"msg":"GetCurrentPackage()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Получить текущий контекст, в котором работает Appium.
	*	Это может быть как "NATIVE_APP" для собственного контекста, так
	*		и для контекста веб-просмотра, который будет:
	*		 * iOS - WEBVIEW_<id>
	*		 * Android - WEBVIEW_<package name>
	*	Для получения информации о контекстах см. Документацию по
	*		гибридной автоматизации Appium.
	*		http://appium.io/docs/en/writing-running-appium/web/hybrid/index.html
	*/
	GetContext() {
		url := Format("{}session/{}/context",this._API,this._sessionID)
		if ((o := this._Get( url )).error)
			throw {"msg":"GetContext()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Получить все контексты, доступные для автоматизации в Appium.
	*	Будет включать, по крайней мере, родной контекст. Также может
	*		быть ноль или более контекстов веб-просмотра.
	*/
	GetContexts() {
		url := Format("{}session/{}/contexts",this._API,this._sessionID)
		if ((o := this._Get( url )).error)
			throw {"msg":"GetContexts()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Устанавливает текущий контекст на переданный. Если при этом происходит
	*		перемещение в контекст веб-представления, это будет включать попытку
	*		подключения к этому веб-представлению.
	*/
	SetContext(name := "NATIVE_APP") {
		temp = {"name":{1}}
		url := Format("{}session/{}/context",this._API,this._sessionID)
		if ((o := this._Post(url, temp := Format(temp, name))).error)
			throw {"msg":"SetContext()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Возвращает массив сессий с их capabilities. 
	*	{"value":[{"capabilities": {...}, "id": "899316a1-047f-4973-bc5f-a4b033d457b2"}, ... ]}
	*/
	GetSessionList() {
		if ((o := this._Get(this._API "sessions")).error ) {
			if (InStr(o.error.Message, "0x80072EFD"))
				throw {"msg":"GetSessionList() - не удается установить соединение с сервером","error":2000}
			throw {"msg":"GetSessionList()", "error":o.error, "request":o.req}
		} Return o.value
	}
	
	/*	
	*	Возвращает base64 строку, представляющую скрин viewport.
	*/
	TakeScreenshot() {
		url := Format("{}session/{}/screenshot",this._API,this._sessionID)
		if ((o := this._Get( url )).error)
			throw {"msg":"TakeScreenshot()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Возвращает объект, "value" которого содержит возможности(capabilities) текущей сессии:
	{
		"status": 0,
		"value": {
			"platform":"LINUX",
			"webStorageEnabled":false,
			"takesScreenshot":true,
			"javascriptEnabled":true,
			"databaseEnabled":false,
			"networkConnectionEnabled":true,
			"locationContextEnabled":false,
			"warnings": {},
			"desired": {
				"app":"C:/full/path/to/com.application.apk",
				"appActivity":".main.MainActivity",
				"appPackage":"com.application",
				"automationName":"Appium",
				"deviceName":"AndroidTestDevice",
				"platformName":"Android",
				"platformVersion":"5.1.1",
				"newCommandTimeout":0,
				"connectHardwareKeyboard":true
			},
			"app":"C:/full/path/to/com.application.apk",
			"appActivity":".main.MainActivity",
			"appPackage":"com.application",
			"automationName":"Appium",
			"deviceName":"emulator-5554",
			"platformName":"Android",
			"platformVersion":"5.1.1",
			"newCommandTimeout":0,
			"connectHardwareKeyboard":true,
			"deviceUDID":"emulator-5554",
			"deviceScreenSize":"480x800",
			"deviceModel":"Android SDK built for x86",
			"deviceManufacturer":"unknown"
		},
		"sessionId":"899316a1-047f-4973-bc5f-a4b033d457b2"
	}
	*/
	GetSessionCapabilities() {
		if ((o := this._Get(this._API "session/" this._sessionID)).error)
			throw {"msg":"GetSessionCapabilities()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Возвращает xml контекста текущей сессии
	*		в виде строки
	*	{"sessionID": "899316a1-047f-4973-bc5f-a4b033d457b2", "status": 0,
	*	"value": "<?xml version="1.0" encoding="UTF-8"?><hierarchy rotation="0"><android.widget.FrameLayout ..."}
	*/
	GetPageSourse() {
		if ((o := this._Get(Format("{}session/{}/source",this._API,this._sessionID))).error)
			throw {"msg":"GetPageSourse()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Возвращает объект: 
	*	{
	*		"altitude": 5,				- высота над уровнем моря
	*		"latitude": 37.422000,		- широта
	*		"longitude": -122.084000	- долгота
	*	}
	*	Работает с типом автоматизации UiAutomator и выше
	*/
	GetGeolocation() {
		if ((o := this._Get(Format("{}session/{}/location",this._API,this._sessionID))).error)
			throw {"msg":"GetGeolocation()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Устанавливает новые значения для геолокации.
	*	Работает с типом автоматизации UiAutomator и выше
	*/
	SetGeolocation(latitude := 37.422, longitude := -122.084, altitude := 5) {
		temp = {"location":{"altitude":{},"latitude":{},"longitude":{3}}}
		;temp = ["location"]
		url := Format("{}session/{}/location",this._API,this._sessionID)
		if ((o := this._Post(url, temp := Format(temp, altitude, latitude, longitude))).error)
			throw {"msg":Format("SetGeolocation('{}')", temp), "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Получает текущие настройки устройства. 
	*		http://appium.io/docs/en/advanced-concepts/settings/index.html
	*	{"imageMatchThreshold":0.4,"fixImageFindScreenshotDims":true,"fixImageTemplateSize":false, ... }
	*	От типа автоматизации девайса зависит количество настроек.
	*/
	GetDeviceSettings() {
		if ((o := this._Get(Format("{}session/{}/appium/settings",this._API,this._sessionID))).error)
			throw {"msg":"GetDeviceSettings()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/* Обновляет настройки устройства. 
	*		http://appium.io/docs/en/advanced-concepts/settings/index.html
	*	В settings - ожидается объект = {"ignoreUnimportantViews":true, ... }
	*	
	*	Булевы true и false, а так же строковый литерал ссылочного(?) типа на
	*		значение null, оборачивайте в кавычки. Перед отправкой, они будут
	*		преобразованы из строкового, в ожидаемый тип.
	*	От типа автоматизации девайса зависит количество настроек.
	*/
	SetDeviceSettings(settings) {
		temp := JSON.Stringify( {"settings": settings} )
		temp := RegExReplace(temp, """(true|false|null)""", "$1")
		url := Format("{}session/{}/appium/settings",this._API,this._sessionID)
		if ((o := this._Post(url, temp)).error)
			throw {"msg":Format("SetDeviceSettings('{}')", temp), "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	
	*	Возвращает true, если виртуальная клавиатура показана, иначе, false.
	*/
	IsKeyboardShown() {
		if ((o := this._Get(Format("{}session/{}/appium/device/is_keyboard_shown",this._API,this._sessionID))).error)
			throw {"msg":"IsKeyboardShown()", "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Скрыть виртуальную клавиатуру.
	*	strategy	- Необязательно. Только для UIAutomation. Может принимать значения:
	*					+ "press"
	*					+ "pressKey"
	*					+ "swipeDown"
	*					+ "tapOut"
	*					+ "tapOutside"
	*					+ "default"
	*/
	HideKeyboard(strategy := "") {
		temp := strategy ? Format("{""strategy"":{1}}", strategy) : ""
		url := Format("{}session/{}/appium/device/hide_keyboard",this._API,this._sessionID)
		if ((o := this._Post(url, temp)).error)
			throw {"msg":Format("HideKeyboard('{}')", temp), "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Отправить текущее приложение для этого сеанса в фоновый режим. 
	*		secs	- целое число секунд, в течении которых приложение
	*				будет выполняться в фоне. Значение равное -1
	*				полностью деактивирует приложение.
	*/
	BackgroundApp(secs) {
		temp = {"secs":{1}}
		url := Format("{}session/{}/appium/settings",this._API,this._sessionID)
		if ((o := this._Post(url, temp := Format(temp, secs))).error)
			throw {"msg":Format("BackgroundApp('{}')", temp), "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Выполнить JavaScript сценарий в контексте текущего выбранного фрейма(Web context only).
	*	http://appium.io/docs/en/commands/web/execute/index.html
	*	
	*	Доступен так же некоторый набор команд для native-app.
	*	http://appium.io/docs/en/commands/mobile-command/index.html
	*	
	*	Возвращает результат выполненного кода.
	*	
	*	cmd:	- строка, представляющая скрипт / mobile:commandName
	*	args:	- JSON-сериализованные аргументы, в виде объекта/массива({}/[])
	*	
	*	Пример:	driver.Execute("window.location.href")
	*	
	*	!!! Не тестировалось. В документации указано как "Не добавлено" !!!
	*/
	Execute(cmd, args := "") {
		temp = {"script":"{}","args":[{2}]}
		url := Format("{}session/{}/execute",this._API,this._sessionID)
		if ((o := this._Post(url, temp := Format(temp, cmd, args))).error)
			throw {"msg":Format("Execute('{}')", temp), "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Выполнить JavaScript сценарий в контексте текущего выбранного фрейма(Web context only)
	*	http://appium.io/docs/en/commands/web/execute-async/index.html
	*	
	*	Предполагается, что исполняемый скрипт является асинхронным и должен сигнализировать о
	*		своём выполнении, вызывая callback, который всегда предоставляется последним
	*		аргументом функции. Значение обратного вызова будет возвращено клиенту.
	*	
	*	Пример:	driver.ExecuteAsync("window.setTimeout(arguments[arguments.length - 1], 500);")
	*	
	*	!!! Не тестировалось. В документации указано как "Не добавлено" !!!
	*/
	ExecuteAsync(cmd, args := "") {
		temp = {"script":"{}","args":[{2}]}
		url := Format("{}session/{}/execute_async",this._API,this._sessionID)
		if ((o := this._Post(url, temp := Format(temp, cmd, args))).error)
			throw {"msg":Format("ExecuteAsync('{}')", temp), "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Отправляет файл девайсу, располагая в его файловой системе по указанному пути.
	*
	*	file 			- имя отправляемого файла, или путь до него
	*	pathToInstall	- полный путь в файловой системе девайса, в который будет сохранён
	*						одноимённый файл, если новое имя фала с расширением, не указано.
	*	
	*	Пример:	driver.PushFile("pictureName.png", "storage/sdcard/Download/")
	*			driver.PushFile("..\MyLib\pictureName.png"
	*											, "storage/sdcard/Download/newPictureName.png")
	*			driver.PushFile("C:\Users\{your_user_name}\Pictures\pictureName.png"
	*											, "storage/sdcard/Download/")
	*	
	*	Существующий файл с таким же именем и расширением будет перезаписан.
	*/
	PushFile(file, pathToInstall) {
		FileGetSize, binLen, %file%
		FileRead, bin, *c %file%
		if (!InStr(pathToInstall, ".")) {
			file := RegExReplace(file, ".*[\\|\/]+?(.*\..*)", "$1")
			pathToInstall := Format("{}/{}", RTrim(pathToInstall, "/\"), file)
		} temp := JSON.Stringify({"path": pathToInstall, "data": this.Base64Encode(bin,binLen)})
		url := Format("{}session/{}/appium/device/push_file", this._API, this._sessionID)
		if ((o := this._Post(url, temp)).error)
			throw {"msg":Format("PushFile('{}')", temp), "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Получает файл из файловой системы девайса, расположенного по указанному пути,
	*		сохраняя в файловой системе компьютера.
	*	
	*	pathOnDevice	- полный путь до файла на устройстве
	*	pathOnComputer	- имя получаемого файла, или путь до него(необязательно)
	*	
	*	Пример:	driver.PullFile("storage/sdcard/Download/pictureName.png")
	*			driver.PullFile("storage/sdcard/Download/pictureName.png", "newPictureName.png")
	*			driver.PullFile("storage/sdcard/Download/pictureName.png"
	*									, "C:\Users\{your_user_name}\Pictures\pictureName.png")
	*	
	*	Создаёт одноимённый файл в месте расположения скрипта, если pathOnComputer не указано.
	*	Существующий файл с таким же именем и расширением будет перезаписан.
	*/
	PullFile(pathOnDevice, pathOnComputer := "") {
		temp := JSON.Stringify( {"path": pathOnDevice} )
		url := Format("{}session/{}/appium/device/pull_file",this._API,this._sessionID)
		if ((o := this._Post(url, temp)).error)
			throw {"msg":Format("PullFile('{}')", temp), "error":o.error, "request":o.req}
		bCount := this.Base64Decode( o.value, bin )
		pathOnComputer := pathOnComputer ? pathOnComputer : RegExReplace(pathOnDevice, ".*[\\|\/]+?(.*\..*)", "$1")
		oFile := FileOpen(pathOnComputer, "w"), oFile.RawWrite(bin, bCount), oFile.Close()
	}
	
	
	/*	Получает папку из файловой системы девайса, расположенную по указанному пути,
	*		сохраняя в файловой системе компьютера в виде ZIP архива.
	*	
	*	pathOnDevice	- полный путь до папки на устройстве
	*	pathOnComputer	- имя архива, или путь до него(необязательно)
	*	
	*	Пример:	driver.PullFile("storage/sdcard/Download/")
	*			driver.PullFile("storage/sdcard/Download/", "newDownload.zip")
	*			driver.PullFile("storage/sdcard/Download/"
	*									, "C:\Users\{your_user_name}\Documents\Download.zip")
	*	
	*	Создаёт одноимённый с конечной папкой ZIP-файл в месте расположения скрипта, если
	*		pathOnComputer не указано.
	*	Существующий файл с таким же именем и расширением будет перезаписан.
	*/
	PullFolder(pathOnDevice, pathOnComputer := "") {
		temp := JSON.Stringify( {"path": pathOnDevice} )
		url := Format("{}session/{}/appium/device/pull_folder",this._API,this._sessionID)
		if ((o := this._Post(url, temp)).error)
			throw {"msg":Format("PullFolder('{}')", temp), "error":o.error, "request":o.req}
		bCount := this.Base64Decode( o.value, bin )
		pathOnComputer := pathOnComputer ? pathOnComputer : RegExReplace(pathOnDevice, ".*\/(.*)\/$", "$1") . ".zip"
		oFile := FileOpen(pathOnComputer, "w"), oFile.RawWrite(bin, bCount), oFile.Close()
	}
	
	Base64Encode(bin, binLen) {
		DllCall("Crypt32.dll\CryptBinaryToString", "Ptr", &bin, "UInt", binLen, "UInt", 0x01, "Ptr", 0, "UIntP", b64Len)
		VarSetCapacity(b64, b64Len << !!A_IsUnicode, 0)
		DllCall("Crypt32.dll\CryptBinaryToString", "Ptr", &bin, "UInt", binLen, "UInt", 0x01, "Ptr", &b64, "UIntP", b64Len)
		VarSetCapacity(b64, -1)
		Return StrReplace(b64, "`r`n")
	}
	
	Base64Decode(b64, ByRef bin) {
		len := StrLen(b64), bCount := 0
		DllCall("Crypt32.dll\CryptStringToBinary","Str",b64,"UInt",len,"UInt",0x1,"UInt",0,"UIntP",bCount,"Int",0,"Int",0)
		VarSetCapacity(bin, bCount, 0)
		DllCall("Crypt32.dll\CryptStringToBinary","Str",b64,"UInt",len,"UInt",0x1,"Ptr",&bin,"UIntP",bCount,"Int",0,"Int",0)
		Return bCount
	}
	
	/*	Отправить SMS на указанный номер телефона.
	*	phoneNumber	- номер телефона получателя
	*	message		- текст сообщения
	*	
	*	В документации дано следующее пояснение:
	*		| Simulate an SMS message (Emulator only)
	*		| http://appium.io/docs/en/commands/device/network/send-sms/index.html
	*	Не теститровалось.
	*/
	SendSMS(phoneNumber, message) {
		temp := JSON.Stringify( {"phoneNumber": phoneNumber, "message": message} )
		url := Format("{}session/{}/appium/device/send_sms",this._API,this._sessionID)
		if ((o := this._Post(url, temp)).error)
			throw {"msg":Format("SendSMS('{}')", temp), "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	Сделать GSM-звонок
	*	phoneNumber	- номер телефона получателя
	*	action		- одно из следующих значений:
	*					+ "call"	= набрать номер
	*					+ "accept"	= принять вызов
	*					+ "cancel"	= отменить звонок
	*					+ "hold"	= удержать
	*	
	*	В документации дано следующее пояснение:
	*		| Make GSM call (Emulator only)
	*		| http://appium.io/docs/en/commands/device/network/gsm-call/index.html
	*	Не тестировалось.
	*/
	MakeGsmCall(phoneNumber, action) {
		temp := JSON.Stringify( {"phoneNumber": phoneNumber, "action": action} )
		url := Format("{}session/{}/appium/device/gsm_call",this._API,this._sessionID)
		if ((o := this._Post(url, temp)).error)
			throw {"msg":Format("MakeGsmCall('{}')", temp), "error":o.error, "request":o.req}
		Return o.value
	}
	
	/*	=============================================================================
	*		Завершение сессии. Отключение Appium от девайса.
	*/
	Quit() {
		oHTTP := ComObjCreate("WinHttp.WinHttpRequest.5.1")
		Try {
			oHTTP.Open("DELETE", this._API "session/" this._sessionID, false)
			oHTTP.Send()
			oHTTP.WaitForResponse()
		} Catch e {
			Return {"error": e}
		} Return oHTTP.Status == 200 ? JSON.Parse(oHTTP.ResponseText) : {"error": oHTTP.Status}
	}
	;	=============================================================================
	
	Class TouchActions
	{
		__New(API, sessionID, appiumDesktop) {
			this.actions := [], this._API := API, this._sessionID := sessionID
			this._ad := appiumDesktop
		}
		
		/*	Методы следующие до LongPress() включительно, реализованы настолько
		*		через жопу, что не работают, принимая аргументы не в том
		*		количестве, что регламентирует официальная документация.
		*		В итоге получается, что передавая указанные аргументы - не
		*		передаёшь все, а если передаёшь все, то нарушаешь шаблон
		*		ожидаемых аргументов.
		*		
		*	В будущих редакциях будут добавлены, когда существующий конфликт
		*		окажется исчерпан. Или удалены вовсе. Следующая за ними
		*		метода, позволяющая собирать несколько действий в один запрос,
		*		выполняет те же функции и в этой связи, необходимость их наличия
		*		весьма сомнительна.
		
		MouseMove(xoffset, yoffset, elementID := "") {
			if (elementID)
				temp = {"element":{3},"xoffset":{1},"yoffset":{2}}
			else
				temp = {"xoffset":{1},"yoffset":{2}}
			url := Format("{}session/{}/moveto",this._API,this._sessionID)
			if ((o := AppiumDriver._Post(url, Format(temp, xoffset, yoffset, elementID))).error)
				throw {"msg":"MouseMove()", "error":o.error, "request":o.req}
			Return o.value
			
		}
		Click(buttonNumber := 0) {
			temp = {"button":"{}"}
			url := Format("{}session/{}/click",this._API,this._sessionID)
			if ((o := AppiumDriver._Post(url, Format(temp,buttonNumber))).error)
				throw {"msg":"Click()", "error":o.error, "request":o.req}
			Return o.value
		}
		
		TouchDown(x, y) {
			temp = {"x":{},"y":{2}}
			url := Format("{}session/{}/touch/down",this._API,this._sessionID)
			if ((o := AppiumDriver._Post(url, Format(temp, x, y))).error)
				throw {"msg":"TouchDown()", "error":o.error, "request":o.req}
			Return o.value
		}
		
		TouchUp(x, y) {
			temp = {"x":{},"y":{2}}
			url := Format("{}session/{}/touch/up",this._API,this._sessionID)
			if ((o := AppiumDriver._Post(url, Format(temp, x, y))).error)
				throw {"msg":"TouchUp()", "error":o.error, "request":o.req}
			Return o.value
		}
		
		Scroll(x, y) {
			temp = {"x":{},"y":{2}}
			url := Format("{}session/{}/touch/scroll",this._API,this._sessionID)
			if ((o := AppiumDriver._Post(url, Format(temp, x, y))).error)
				throw {"msg":"Scroll()", "error":o.error, "request":o.req}
			Return o.value
		}
		
		LongPress(element) {
			temp := JSON.Stringify({"elements": element.element})
			url := Format("{}session/{}/touch/longclick",this._API,this._sessionID)
			if ((o := AppiumDriver._Post(url, temp)).error)
				throw {"msg":"LongPress()", "error":o.error, "request":o.req}
			Return o.value
		}
		*/
		
		/*	Имитирует движение пальцем по viewport. Используйте, если не критично
		*		начальное положение пальца.
		*	Параметры, обозначают количество пикселей, которые должен пройти
		*		"палец" до отпускания. Сохраняет инерцию.
		*	xspeed:	положительное - пальцем вправо(viewport влево)
		*				отрицательное - влево
		*	yspeed:	положительное - пальцем вниз(viewport вверх)
		*				отрицательное - вверх
		*	
		*	Требует способ автоматизации - UiAutomator2
		*	
		*	Может вызывать исключение "JSONException: No value for xSpeed", если сервер запущен
		*		из CMD.exe и "JSONException: No value for xspeed", если из десктоп-версии, когда
		*		имена полей не соответствуют ожидаемому регистру символов.
		*/
		Flick(xspeed := 0, yspeed := -100) {
			temp := JSON.Stringify( this._ad
								? {"xspeed": xspeed, "yspeed": yspeed}
								: {"xSpeed": xspeed, "ySpeed": yspeed} )
			url := Format("{}session/{}/touch/flick", this._API,this._sessionID)
			if ((o := AppiumDriver._Post(url, temp )).error)
				throw {"msg":Format("action.Flick('{}')", temp), "error":o.error, "request":o.req}
			Return o.value
		}
		
		;	###########################################################################
		
		/*	Ниже приведены методы, позволяющие собирать несколько действий
		*		в один запрос. Например, FlickDown() и FlickUp() - это
		*		наборы, описывающие движение пальцем по viewport,
		*		прокручивающие последний на 10 пикселей вниз, или вверх
		*		соответственно, а LongTap() - держит палец в координатах
		*		с короткой паузой и отпускает.
		*	В отличии от action.Flick() и element.Flick(), эта имитация
		*		более схожа с реальным движением, так как сохраняет
		*		инерцию прокручивания после "отпускания", в результате
		*		чего, точность прокрутки не обеспечивается.
		*	
		*	Tap(x, y)		- одиночное касание в x и y координатах.
		*	Wait(ms)		- ожидание в миллисекундах до следующего действия.
		*	Press(x, y)		- опускает палец в x и y координатах.
		*	MoveTo(x, y)	- перемещает палец в x и y координаты.
		*	Release()		- отпускает палец.
		*	PerformAll()	- формирует запрос из описанного набора.
		*/
		Tap(x, y) {
			this.actions.Push( {"action":"tap","options":{"x":x,"y":y}} )
		} Wait(ms) {
			this.actions.Push( {"action":"wait","options":{"ms":ms}} )
		} Press(x, y) {
			this.actions.Push( {"action":"press","options":{"x":x,"y":y}} )
		} MoveTo(x, y) {
			this.actions.Push( {"action":"moveTo","options":{"x":x,"y":y}} )
		} Release() {
			this.actions.Push( {"action":"release","options":{}} )
		} FlickDown(from_x := 1, from_y := 200, to_x := 1, to_y := 190, ms := 0) {
			this.Press(from_x, from_y), (ms && this.Wait(ms)), this.MoveTo(to_x, to_y), this.Release()
			this.PerformAll()
		} FlickUp(from_x := 1, from_y := 200, to_x := 1, to_y := 210, ms := 0) {
			this.Press(from_x, from_y), (ms && this.Wait(ms)), this.MoveTo(to_x, to_y), this.Release()
			this.PerformAll()
		} LongTap(x, y, ms := 1000) {
			this.Press(x, y), this.Wait(ms), this.Release()
			this.PerformAll()
		} PerformAll() {
			url	 := Format("{}session/{}/touch/perform",this._API,this._sessionID)
			temp := JSON.Stringify(this.actions), this.actions := []
			if ((o := AppiumDriver._Post(url, temp)).error)
				throw {"msg":Format("PerformAll('{}')", temp), "error":o.error, "request":o.req}
			Return o
		}
	}
}

Class __SearchContext
{
	/*	https://www.w3.org/TR/1999/REC-xpath-19991116/
	*	Возвращает первый найденный элемент по локатору xPath
	*	Возвращаемый результат:
	*	{"status":0,"value":{"element-6066-11e4-a52e-4f735466cecf":"17","ELEMENT":"17"}
	*	,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
	*/	
	FindElementByXPath(xPath) {
		temp = {"using":"xpath","value":"{}"}
		url := Format("{}session/{}/element",this._API,this._sessionID)
		if ((o := this._Post(url, Format(temp, xPath))).error && o.error != 500)
			throw {"msg":Format("FindElementByXPath('{}')", xPath), "error":o.error, "request":o.req}
		Return new this._Element( o.value, this._API, this._sessionID )
	}
	
	/*	Возвращает массив элементов, найденных по локатору xPath
	*	Возвращаемый результат:
	*	{"status":0,"value":[{"element-6066-11e4-a52e-4f735466cecf":"17","ELEMENT":"17"}]
	*	,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
	*/	
	FindElementsByXPath(xPath) {
		temp = {"using":"xpath","value":"{}"}
		url := Format("{}session/{}/elements",this._API,this._sessionID)
		if ((o := this._Post(url, Format(temp, xPath))).error && o.error != 500)
			throw {"msg":Format("FindElementsByXPath('{}')", xPath), "error":o.error, "request":o.req}
		Return new this._Element( o.value, this._API, this._sessionID )
	}
	
	/*	Возвращает элемент, найденный по ID
	*	Возвращаемый результат:
	*	{"status":0,"value":{"element-6066-11e4-a52e-4f735466cecf":"17","ELEMENT":"17"}
	*	,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
	*/	
	FindElementById(id) {
		temp = {"using":"id","value":"{}"}
		url := Format("{}session/{}/element",this._API,this._sessionID)
		if ((o := this._Post(url, Format(temp, id))).error && o.error != 500)
			throw {"msg":Format("FindElementsById('{}')", id), "error":o.error, "request":o.req}
		Return new this._Element( o.value, this._API, this._sessionID )
	}
	
	/*	Возвращает массив элементов, найденных по по ID
	*	Возвращаемый результат:
	*	{"status":0,"value":[{"element-6066-11e4-a52e-4f735466cecf":"17","ELEMENT":"17"}]
	*	,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
	*/	
	FindElementsById(id) {
		temp = {"using":"id","value":"{}"}
		url := Format("{}session/{}/elements",this._API,this._sessionID)
		if ((o := this._Post(url, Format(temp, id))).error && o.error != 500)
			throw {"msg":Format("FindElementsById('{}')", id), "error":o.error, "request":o.req}
		Return new this._Element( o.value, this._API, this._sessionID )
	}
	
	/*	Возвращает элемент, найденный по className
	*	Возвращаемый результат:
	*	{"status":0,"value":{"element-6066-11e4-a52e-4f735466cecf":"17","ELEMENT":"17"}
	*	,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
	*/	
	FindElementByClassName(className) {
		temp = {"using":"class name","value":"{}"}
		url := Format("{}session/{}/element",this._API,this._sessionID)
		if ((o := this._Post(url, Format(temp, className))).error && o.error != 500)
			throw {"msg":Format("FindElementByClassName('{}')", className), "error":o.error, "request":o.req}
		Return new this._Element( o.value, this._API, this._sessionID )
	}
	
	/*	Возвращает массив элементов, найденных по className
	*	Возвращаемый результат:
	*	{"status":0,"value":[{"element-6066-11e4-a52e-4f735466cecf":"17","ELEMENT":"17"}, ...]
	*	,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
	*/	
	FindElementsByClassName(className) {
		temp = {"using":"class name","value":"{}"}
		url := Format("{}session/{}/elements",this._API,this._sessionID)
		if ((o := this._Post(url, Format(temp, className))).error && o.error != 500)
			throw {"msg":Format("FindElementsByClassName('{}')", className), "error":o.error, "request":o.req}
		Return new this._Element( o.value, this._API, this._sessionID )
	}
	
	/*	Возвращает элемент, найденный по accessibilityId. Для XCUITest это содержимое
	*		атрибута 'accessibility-id'. Для Android 'content-desc'.
	*	Возвращаемый результат:
	*	{"status":0,"value":{"element-6066-11e4-a52e-4f735466cecf":"17","ELEMENT":"17"}
	*	,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
	*/	
	FindElementByAccessibilityId(accessibilityId) {
		temp = {"using":"accessibility id","value":"{}"}
		url := Format("{}session/{}/element",this._API,this._sessionID)
		if ((o := this._Post(url, Format(temp, accessibilityId))).error && o.error != 500)
			throw {"msg":Format("FindElementByAccessibilityId('{}')", accessibilityId), "error":o.error, "request":o.req}
		Return new this._Element( o.value, this._API, this._sessionID )
	}
	
	/*	Возвращает массив элементов, найденных по accessibilityId. Для XCUITest это содержимое
	*		атрибута 'accessibility-id'. Для Android 'content-desc'.
	*	Возвращаемый результат:
	*	{"status":0,"value":[{"element-6066-11e4-a52e-4f735466cecf":"17","ELEMENT":"17"}, ...]
	*	,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
	*/	
	FindElementsByAccessibilityId(accessibilityId) {
		temp = {"using":"accessibility id","value":"{}"}
		url := Format("{}session/{}/elements",this._API,this._sessionID)
		if ((o := this._Post(url, Format(temp, accessibilityId))).error && o.error != 500)
			throw {"msg":Format("FindElementsByAccessibilityId('{}')", accessibilityId), "error":o.error, "request":o.req}
		Return new this._Element( o.value, this._API, this._sessionID )
	}
	
	/*	Используется "UiAutomator Api", драйвера для Android - UiAutomator2.
	*		http://appium.io/docs/en/drivers/android-uiautomator2/
	*	Возвращает элемент, найденный по UiSelector, позволяя так же осуществлять прокручивания
	*		элементов средствами 'UiScrollable' в одном запросе.
	*	В качестве селекторов поддерживаются:
	*		UiSelector - 
	*			https://developer.android.com/reference/android/support/test/uiautomator/UiSelector
	*		UiScrollable - 
	*			https://developer.android.com/reference/android/support/test/uiautomator/UiScrollable
	*	Возвращаемый результат:
	*	{"status":0,"value":{"element-6066-11e4-a52e-4f735466cecf":"17","ELEMENT":"17"}
	*	,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
	*
	*	Из офф. документации:
	*		| Note: This framework requires Android 4.3 (API level 18) or higher.
	*		| Заметка: Этот фреймворк поддерживает Android версии 4.3 (уровня API 18) и выше.
	*	
	*	Чтобы иметь возможность использовать средства "UiAutomator Api", необходимо её включить
	*		в "capabilities" сессии, установив в поле "automationName", значение "UiAutomator2"
	*		(по умолчанию Appium). Так же, обязательно,"platformName" должна быть установлена как
	*		"Android", указана "platformVersion", "deviceName" и "app".
	*	
	*	Примеры:
	*		- Найти элемент, видимый текст которого = 'Text on element'
	*			driver.FindElementByUiAutomator("new UiSelector().text(""Text on element"")")
	*		- Найти первый прокручиваемый элемент, затем, найти его дочерний элемент с текстовым
	*			полем, содержащим текст 'Tabs'. Элемент 'Tabs' должен быть в поле зрения.
	*			driver.FindElementByUiAutomator("new UiScrollable(new UiSelector().scrollable(true).instance(0)).getChildByText(new UiSelector().className(""android.widget.TextView""), ""Tabs"")")
	*		- Найти элемент по 'resourceId' и прокрутить вниз/вперёд(зависит от ориентации девайса).
	*			driver.FindElementByUiAutomator("new UiScrollable(new UiSelector().resourceId(""com.android.resource:id/id"")).scrollForward(10)")
	*		- Найти потомка 'resourceId' по 'className'.
	*			driver.FindElementByUiAutomator("new UiSelector().resourceId(""com.android.resource:id/id"").childSelector(new UiSelector().className(""android.widget.TextView""))")
	*	Обращение к унаследованным методам в UiSelector - не поддерживается и будет
	*		возвращать пустое значение.
	*/	
	FindElementByUiAutomator(UiSelector) {
		temp := {"using":"-android uiautomator","value":UiSelector}
		url := Format("{}session/{}/element",this._API,this._sessionID)
		if ((o := this._Post(url, JSON.Stringify(temp))).error && o.error != 500)
			throw {"msg":Format("FindElementByUiAutomator('{}')", UiSelector), "error":o.error, "request":o.req}
		Return new this._Element( o.value, this._API, this._sessionID )
	}
	
	/*	Только для Android! Возвращает массив элементов, найденных по UiSelector.
	*	С помощью 'UiAutomator' невозможно запросить коллекцию элементов, формирует которую
	*		не поддерживаемый 'UiCollection'. Будет возвращён массив с одним элементом, если
	*		поиск был удачен.
	*	Возвращаемый результат:
	*	{"status":0,"value":[{"element-6066-11e4-a52e-4f735466cecf":"17","ELEMENT":"17"}, ...]
	*	,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
	*/
	FindElementsByUiAutomator(UiSelector) {
		temp := {"using":"-android uiautomator","value":UiSelector}
		url := Format("{}session/{}/elements",this._API,this._sessionID)
		if ((o := this._Post(url, JSON.Stringify(temp))).error && o.error != 500)
			throw {"msg":Format("FindElementsByUiAutomator('{}')", UiSelector), "error":o.error, "request":o.req}
		Return new this._Element( o.value, this._API, this._sessionID )
	}
		
	/*	Получить активный элемент. Пока не реализован.
	*	http://appium.io/docs/en/commands/element/other/active/index.html
	*
	GetActiveElement() {
		url := Format("{}session/{}/element/active", this._API,this._sessionID)
		if ((o := this._Post( url )).error)
			throw Format("GetActiveElement() завершился ошибкой '{}'`n{}", this.element, o.error, o.req)
		Return new this._Element( o.value, this._API, this._sessionID )
	}
	*/
	
	_Get(url) {
		oHTTP := ComObjCreate("WinHttp.WinHttpRequest.5.1")
		Try {
			oHTTP.Open("GET", url, false)
			oHTTP.Send()
			oHTTP.WaitForResponse()
		} Catch e {
			if (InStr(e.Message, "0x80072F78")		; Сервер вернул недопустимый или нераспознанный ответ
				|| InStr(e.Message, "0x80072EE2")	; Время ожидания операции истекло
				|| InStr(e.Message, "0x80072EFE"))	; Соединение с сервером было неожиданно прервано
				Return {"error": "0x80072F78|0x80072EE2|0x80072EFE"}
			Return {"error": e}
		} Return oHTTP.Status == 200 ? JSON.Parse(oHTTP.ResponseText) : {"error": oHTTP.Status,"req": oHTTP.ResponseText}
	}
	
	_Post(url, sJson := "") {
		oHTTP := ComObjCreate("WinHttp.WinHttpRequest.5.1")
		Try {
			oHTTP.Open("POST", url, false)
			oHTTP.SetRequestHeader("Content-Type", "application/json; charset=UTF-8")
			oHTTP.Send(sJson)
			oHTTP.WaitForResponse()
		} Catch e {
			if (InStr(e.Message, "0x80072F78")		; Сервер вернул недопустимый или нераспознанный ответ
				|| InStr(e.Message, "0x80072EE2")	; Время ожидания операции истекло
				|| InStr(e.Message, "0x80072EFE"))	; Соединение с сервером было неожиданно прервано
				Return {"error": "0x80072F78|0x80072EE2|0x80072EFE"}
			Return {"error": e}
		} Return oHTTP.Status == 200 ? JSON.Parse(oHTTP.ResponseText) : {"error": oHTTP.Status,"req": oHTTP.ResponseText}
	}
	

2

Re: AHK: AppiumDriver Class. Android/IOS automation.

Не влезло. Продолжение:


	Class _Element
	{
		__New(o, API, sessionID) {
			if (!o.Length()) {
				this.element := o.ELEMENT, this._API := API, this._sessionID := sessionID
				Return o.element ? this : ""
			} else {
				elements := []
				For k, v in o
					elements.Push(new this(v, API, sessionID))
				Return elements
			}
		}
		
		/*	Имитирует движение пальцем по viewport, начинаясь на выбранном элементе.
		*	xoffset и yoffset - количество пикселей, которые нужно пройти от позиции
		*		элемента(его верхний левый угол), до остановки. Ниболее точен при
		*		значениях speed, около 10.
		*	xoffset:	положительное - пальцем вправо(viewport влево)
		*				отрицательное - влево
		*	yoffset:	положительное - пальцем вниз(viewport вверх)
		*				отрицательное - вверх
		*	speed:		скорость в пикселях в секунду
		*/
		Flick(xoffset := 0, yoffset := -100, speed := 100) {
			temp = {"xoffset":{},"yoffset":{},"element":"{}","speed":{4}}
			url := Format("{}session/{}/touch/flick", this._API,this._sessionID,this.element)
			sJson := Format(temp, xoffset, yoffset, this.element, speed)
			if ((o := __SearchContext._Post(url, sJson)).error)
				throw {"msg":"element.Flick()", "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Сообщить элементу событие 'клик'.
		*	{"status":0,"value":true,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		Click() {
			url := Format("{}session/{}/element/{}/click", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Post( url )).error)
				throw {"msg":"element.Click()", "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Установить строку в editable поле элемента.
		*	{"status":0,"value":true,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*	Найдено в единственном месте:
		*	https://github.com/appium/appium-base-driver/blob/master/lib/protocol/routes.js#L548
		*/
		SetValue(string) {
			temp := {"value":[string]}
			url := Format("{}session/{}/appium/element/{}/value", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Post(url, JSON.Stringify(temp))).error)
				throw {"msg":Format("element.SetValue('{}')", string), "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Отправить строку которая будет набрана в editable поле элемента.
		*	{"status":0,"value":true,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		SendKeys(string) {
			temp := {"value":[string]}
			url := Format("{}session/{}/element/{}/value", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Post(url, JSON.Stringify(temp))).error)
				throw {"msg":Format("element.SendKeys('{}')", string), "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Заменить строку в editable поле элемента.
		*	{"status":0,"value":true,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		ReplaceValue(string) {
			temp := {"value":[string]}
			url := Format("{}session/{}/appium/element/{}/replace_value", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Post(url, JSON.Stringify(temp))).error)
				throw {"msg":Format("element.ReplaceValue('{}')", string), "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Очистить элемент. Как правило - editable.
		*	{"status":0,"value":null,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		Clear() {
			url := Format("{}session/{}/element/{}/clear", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Post( url )).error)
				throw {"msg":"element.Clear()", "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Сообщить форме Submit()
		*	{"status":0,"value":null,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*	Только для Mac(?) и Windows(10+)
		*		| http://appium.io/docs/en/commands/element/other/submit/index.html
		*/
		Submit() {
			url := Format("{}session/{}/element/{}/submit", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Post( url )).error)
				throw {"msg":"element.Submit()", "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Получить видимый текст элемента.
		*	{"status":0,"value":"Text from element","sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		GetText() {
			url := Format("{}session/{}/element/{}/text", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Get( url )).error)
				throw {"msg":"element.GetText()", "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Получить имя тега.
		*	{"status":0,"value":"android.widget.EditText","sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		GetTagName() {
			url := Format("{}session/{}/element/{}/name", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Get( url )).error)
				throw {"msg":"element.GetTagName()", "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Позиция элемента после прокручивания View(doc).
		*	{"status":0,"value":{"x":108,"y":53},"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		GetLocationInView() {
			url := Format("{}session/{}/element/{}/location_in_view", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Get( url )).error)
				throw {"msg":"element.GetLocationInView()", "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Позиция элемента относительно верхнего левого угла View.
		*	{"status":0,"value":{"x":108,"y":53},"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		GetLocation() {
			url := Format("{}session/{}/element/{}/location", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Get( url )).error)
				throw {"msg":"element.GetLocation()", "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Длина и ширина элемента
		*	{"status":0,"value":{"width":227,"height":54},"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		GetSize() {
			url := Format("{}session/{}/element/{}/size", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Get( url )).error)
				throw {"msg":"element.GetSize()", "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	GetLocation() и GetSize() - в одном флаконе
		*	{"status":0,"value":{"x":108,"y":53,"width":227,"height":54},"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		GetRect() {
			url := Format("{}session/{}/element/{}/rect", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Get( url )).error)
				throw {"msg":"element.GetRect()", "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	
		*	Вероятно, только для WEB контекста.
		*/
		GetCSSProperty(prop) {
			url := Format("{}session/{}/element/{}/css/{}", this._API,this._sessionID,this.element,prop)
			if ((o := __SearchContext._Get( url )).error)
				throw {"msg":Format("element.GetCSSProperty('{}')", prop), "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Получить значение атрибута attr. Доступно только при автоматизации через UiAutomator2
		*	{"status":0,"value":"false","sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		GetAttribute(attr := "class") {
			url := Format("{}session/{}/element/{}/attribute/{}", this._API,this._sessionID,this.element,attr)
			if ((o := __SearchContext._Get( url )).error)
				throw {"msg":Format("element.GetAttribute('{}')", attr), "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Возвращает состояние элемента.
		*	Для selectable элементов вроде radio, checkbox, etc...
		*	{"status":0,"value":true,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		IsSelected() {
			url := Format("{}session/{}/element/{}/selected", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Get( url )).error)
				throw {"msg":"element.IsSelected()", "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Доступен для взаимодействия?
		*	{"status":0,"value":true,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		IsEnabled() {
			url := Format("{}session/{}/element/{}/enabled", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Get( url )).error)
				throw {"msg":"element.IsEnabled()", "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Показан?
		*	{"status":0,"value":true,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		IsDisplayed() {
			url := Format("{}session/{}/element/{}/displayed", this._API,this._sessionID,this.element)
			if ((o := __SearchContext._Get( url )).error)
				throw {"msg":"element.IsDisplayed()", "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
		
		/*	Проверить, является ли переданный элемент, тем же самым элементом.
		*		Может быть не реализован, по причине того, что даже при запросе на поиск элемента
		*		по одному и тому же идентификатору, сервер генерирует всегда новый ID элемента.
		*			| https://github.com/appium/java-client/issues/227
		*		
		*	{"status":0,"value":true,"sessionId":"e6f8a520-ea5c-4eec-b7f9-1fd9ab217837"}
		*/
		AreEqual(otherElement) {
			url := Format("{}session/{}/element/{}/equals/{}", this._API,this._sessionID, this.element, otherElement.element)
			if ((o := __SearchContext._Get( url )).error)
				throw {"msg":Format("element.AreEqual('{}')", otherElement.element), "element":this.element, "error":o.error, "request":o.req}
			Return o.value
		}
	}
}

3 (изменено: KusochekDobra, 2019-02-22 16:31:28)

Re: AHK: AppiumDriver Class. Android/IOS automation.

Пример работы, демонстрируемый выше под спойлером:


#SingleInstance, Force
#NoEnv
SetWorkingDir, A_ScriptDir
ListLines, Off
SetBatchLines, -1

#Include <JSON>
#Include <AppiumDriver>
OnMessage(0x201, "WM_LBUTTONDOWN")

; >>--++--<< Минимальные capabilities, необходимые для запуска теста >>--++--<<
	caps =
	(
		{
			"desiredCapabilities": {
				"platformName": "Android",
				"deviceName": "SelendroidTester",
				"app": "{}/selendroid-test-app-0.17.0.apk"
			}
		}
	)
	caps := Format(caps, StrReplace(A_ScriptDir, "\", "/"))
; >>--++--<<  >>--++--<<
	gui_w := 500, gui_h := 220
	defaultPort := 4723, aDesktop := true
	autoConnect := false
; >>--++--<<  >>--++--<<
	guiColor := 0xA9A5A0
; >>--++--<<  >>--++--<<

Gui,1: Color,% guiColor
Gui,1: -Caption +HWNDmain_h
Gui,1: Margin, 10, 10
Gui,1: Add, Button,% Format("x{} ym-10 w{} h{} gReloadMe",gui_w-63,20,18),R
Gui,1: Add, Button,% Format("x+0 ym-10 w{} h{} gMiniMe",20,18),__
Gui,1: Add, Button,% Format("x+0 ym-10 w{} h{} gGuiClose",20,18),X
Gui,1: Font, Bold, Consolas
Gui,1: Add, Text,% Format("xm ym+15 w{} r6 Border v_statusText cBlue",gui_w - 20),Текст
Gui,1: Add, Text,% Format("xm y+5 w{} Border gCopySessionID v_sessionID cRed Border 0x0201",gui_w - 20),Ожидается идентификатор сессии ...
Gui,1: Font

Gui,1: Add, Button,% Format("xm y{} gResetApp",gui_h-80),ResetApp
Gui,1: Add, Button,x+10 gStopApp,StopApp
Gui,1: Add, Button,x+10 gStartApp,StartApp
Gui,1: Add, Button,x+10 gRunServer,Запустить сервер

Gui,1: Font, Bold
Gui,1: Add, Button,% Format("xm y{} v_runMB gRunMain",gui_h-50),Run MainBody
Gui,1: Font
Gui,1: Add, Button,x+10 gGetCurrentActivity,Get activity
Gui,1: Add, Button,x+10 gStartSession,Подключиться/Создать сессию
Gui,1: Add, Button,x+10 gStopSession,Завершить сессию

Gui,1: Add, StatusBar
Gui,1: Show,w%gui_w% h%gui_h%,Selendroid Tester
FrameShadow( main_h )
; >>--++--<<  >>--++--<<
	if (autoConnect) {
		SB_SetText("Подключение к Appium ...")
		Try
			GoSub, ConnectAppium
		Catch e {
			if (e.error == 1000)
				SB_SetText("Нет запущенных серверов")
		}
	}
; >>--++--<<  >>--++--<<
Return
GetCurrentActivity:
	SB_SetText("Текущая activity:`t" . (Clipboard := driver.GetCurrentActivity()))
Return
RunMain:
	if (thread_toggler := !thread_toggler) {
		Gui,1: Submit, NoHide
		SetTimer,MainBody,-1
		GuiControl,Text,_runMB,Stop MainBody
	} else {
		GuiControl,Text,_runMB,Run MainBody
	}
Return
ConnectAppium:
	Try {
		driver := new AppiumDriver(, defaultPort, aDesktop)
		SB_SetText("Подключён к текущей сессии")
	} Catch {
		driver := new AppiumDriver(caps, defaultPort, aDesktop)
		SB_SetText("Создана новая сессия")
	}
	GuiControl,Text,_sessionID,% "Идентификатор сессии:`t" driver._sessionID
Return
CopySessionID:
	if (A_GuiControlEvent == "DoubleClick") {
		if (driver) {
			Clipboard := driver._sessionID
			SB_SetText("ID сессии скопирован в буфер обмена")
		} else
			SB_SetText("Соединение не установлено")
	}
Return
;	###############################################################################################
MainBody:
{
	GuiControl,Text,_statusText,Ожидание кнопки с логотипом папки ...
	While !(folderButton := driver.FindElementByAccessibilityId("startUserRegistrationCD"))
		Sleep(1000)
	GuiControl,Text,_statusText,Кнопка найдена. Клик!
	Sleep(1000)
	folderButton.Click()
	GuiControl,Text,_statusText,Заполняем форму
	Sleep(1000)
	if (driver.IsKeyboardShown())
		driver.HideKeyboard()
	editUsername := driver.FindElementById("io.selendroid.testapp:id/inputUsername")
	editUsername.SetValue(A_UserName)
	driver.FindElementById("io.selendroid.testapp:id/inputEmail").ReplaceValue("script_coding@example_mail.ru")
	if (driver.IsKeyboardShown())
		driver.HideKeyboard()
	driver.FindElementById("io.selendroid.testapp:id/inputPassword").SendKeys("veryStrongPass!!11")
	if (driver.IsKeyboardShown())
		driver.HideKeyboard()
	mr_burns := driver.FindElementById("io.selendroid.testapp:id/inputName")
	mr_burns.Clear()
	if (driver.IsKeyboardShown())
		driver.HideKeyboard()
	mr_burns.SendKeys("Hello Android from AutoHotKey!")
	if (driver.IsKeyboardShown())
		driver.HideKeyboard()
	driver.FindElementById("io.selendroid.testapp:id/input_preferedProgrammingLanguage").Click()
	
	GuiControl,Text,_statusText,Выбираем 'Си Шарп'
	While !(listview_el := driver.FindElementById("android:id/select_dialog_listview"))
		Sleep(1000)
	While !(cSharp := driver.FindElementByXPath("//android.widget.CheckedTextView[contains(@text,'C#')]")) {
		listview_el.Flick(0, -300)
		Sleep(1000)
	}
	
	cSharp.Click()
	Sleep(1000)
	
	driver.FindElementByClassName("android.widget.ScrollView").Flick(0, -300)
	
	Sleep(1000)
	driver.FindElementById("io.selendroid.testapp:id/input_adds").Click()
	Sleep(1000)
	driver.FindElementById("io.selendroid.testapp:id/btnRegisterUser").Click()
	
	GuiControl,Text,_statusText,Выбираем текстовые поля с введёнными только что данными
	Sleep(1000), data := []
	textFields := driver.FindElementsByXPath("//android.widget.TableRow/android.widget.TextView[@index='1']")
	For index, element in textFields
		data.Push( element.GetText() )
	string := Format("Name '{}' | UserName '{}' | Password '{}' | E-Mail '{}' | Programming Language '{}' | I accept adds '{}'", data*)
	GuiControl,Text,_statusText,% string
	
	/*
	name		:= driver.FindElementById("io.selendroid.testapp:id/label_name_data").GetText()
	userName	:= driver.FindElementById("io.selendroid.testapp:id/label_username_data").GetText()
	pass		:= driver.FindElementById("io.selendroid.testapp:id/label_password_data").GetText()
	email		:= driver.FindElementById("io.selendroid.testapp:id/label_email_data").GetText()
	pLang		:= driver.FindElementById("io.selendroid.testapp:id/label_preferedProgrammingLanguage_data").GetText()
	accept		:= driver.FindElementById("io.selendroid.testapp:id/label_acceptAdds_data").GetText()
	*/
	
	MsgBox, В лог выбраны строки со страницы верификации пользователя. Продолжаем?
	driver.FindElementById("io.selendroid.testapp:id/buttonRegisterUser").Click()
	GuiControl,Text,_runMB,Run MainBody
	thread_toggler := !thread_toggler
	MsgBox, Тестовый образец кода завершён.
}
;SetTimer,MainBody,-1
Return
;	###############################################################################################
Sleep(Delay) {
	Global thread_toggler
	Start := A_TickCount
 	While A_TickCount - Start < Delay && thread_toggler
		Sleep 1
	If !(thread_toggler)
		Exit
}
WM_LBUTTONDOWN()  {
   PostMessage, WM_NCLBUTTONDOWN := 0xA1, HTCAPTION := 2
}
FrameShadow(HGui) {
	DllCall("dwmapi\DwmIsCompositionEnabled","IntP",_ISENABLED)
	if !_ISENABLED
		DllCall("SetClassLong","UInt",HGui,"Int",-26,"Int",DllCall("GetClassLong","UInt",HGui,"Int",-26)|0x20000)
	else {
		VarSetCapacity(_MARGINS,16)
		NumPut(1,&_MARGINS,0,"UInt")
		NumPut(1,&_MARGINS,4,"UInt")
		NumPut(1,&_MARGINS,8,"UInt")
		NumPut(1,&_MARGINS,12,"UInt")
		DllCall("dwmapi\DwmSetWindowAttribute", "Ptr", HGui, "UInt", 2, "Int*", 2, "UInt", 4)
		DllCall("dwmapi\DwmExtendFrameIntoClientArea", "Ptr", HGui, "Ptr", &_MARGINS)
	}
}
;	==================================================================================================
;		Подпрограммы
;	==================================================================================================
Return
RunServer:
	if (aDesktop) {
		if (id := WinExist("ahk_exe Appium.exe")) {
			WinActivate,ahk_id %id%
			SB_SetText("UI версия сервера уже запущена")
			Return
		}
		SB_SetIcon("Запускаем UI версия сервера ...")
		Run,% Format("C:\Users\{}\AppData\Local\Programs\Appium\Appium.exe",A_UserName)
		Return
	}
	if (!FileExist(moduleFolder := Format("C:\Users\{}\AppData\Roaming\npm\node_modules\appium",A_UserName))) {
		MsgBox,16,Ошибка!,NodeJS модуль 'Appium' не установлен`, или услановлен не по ожидаемому пути:`n%moduleFolder%
		Return
	}
	
	InputBox, newPort,Порт,Порт сервера. Если это новая сессия`, подойдёт дефолтный.,,,150,,,,,% defaultPort
	if (ErrorLevel) {
		MsgBox,,Отмена операции.,Попробуйте снова.
		Return
	}
	SetWorkingDir, %moduleFolder%
	Run, % "cmd /c " . Format("appium --address 127.0.0.1 --port {}", newPort)
	SetWorkingDir, %A_ScriptDir%
Return
ResetApp:
	driver.ResetApp()
Return
StopApp:
	driver.CloseApp()
Return
StartApp:
	driver.LaunchApp()
Return
MiniMe:
	WinMinimize, ahk_id%main_h%
return
ReloadMe:
	Reload
GuiClose:
	ExitApp
StartSession:
	GoSub, ConnectAppium
Return
StopSession:
	driver.Quit()
	driver := ""
	GuiControl,Text,_sessionID,Сессия завершена
	SB_SetText("Отключен от Appium")
Return

4

Re: AHK: AppiumDriver Class. Android/IOS automation.

Интересно ваше мнение, коллеги. Какие места требуют пересмотра?

5

Re: AHK: AppiumDriver Class. Android/IOS automation.

Выглядит многообещающе, спасибо! Если будет время потестировать, отпишусь.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

6

Re: AHK: AppiumDriver Class. Android/IOS automation.

Для тех кто в глухом танке и забанены в гугле за лень, это только для эмуляторов на ПК.

KusochekDobra пишет:

Не влезло. Продолжение:

Почему не ГитХаб?

По вопросам возмездной помощи пишите на E-Mail: serzh82saratov@mail.ru Telegram: https://t.me/sergiol982
Win10x64 AhkSpy, Hotkey, ClockGui

7

Re: AHK: AppiumDriver Class. Android/IOS automation.

serzh82saratov пишет:

это только для эмуляторов на ПК

В смысле, телефоном с компьютера так нельзя управлять?

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

8

Re: AHK: AppiumDriver Class. Android/IOS automation.

Ну почему же, выше, я обозначил нижний предел API для Android, но не говорил, что это только для эмуляторов. В интернете множество примеров работы Appium с различными девайсами, так как разрабатывается он с целью проведения тестов программного обеспечения, просто виртуализация позволяет эмулировать разные среды и охватывать большее количество багов на одной машине.

Проверить доступность для автоматизации своего телефона можно в консоли, писал об этом выше. Мобилка должна быть подключена по USB и включена возможность отладки, для чего на ней так же должен быть активен "режим разработчика".

serzh82saratov, это важное условие?

9

Re: AHK: AppiumDriver Class. Android/IOS automation.

С эмуляторами кстати, должно быть сложнее, так как эмуляторы типа "NoxApp" c ADB не коннектятся. Мне не удалось запустить ни одной сессии в Appium. Это говорит только в пользу тех эмуляторов, что наиболее приближены своей реализацией к настоящим устройствам.

10

Re: AHK: AppiumDriver Class. Android/IOS automation.

Ещё один интересный момент в том, что, поскольку Appium - это сервер, мне встречались так же статьи, в которых описывалась его работа удалённо. в том числе, в одной локальной сети. Например, с компьютера управлялся девайс, подключённый к тому же маршрутизатору.

11

Re: AHK: AppiumDriver Class. Android/IOS automation.

Вот это было бы интереснее.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

12

Re: AHK: AppiumDriver Class. Android/IOS automation.

KusochekDobra, работа вами проделана большая.
А в чем у вас практическое применение, если не секрет?

13

Re: AHK: AppiumDriver Class. Android/IOS automation.

teadrinker
Appium: Connecting Android device through WIFI. Не проверял, но, это должно быть единственный способ. В конструкторе класса используется локалхост для связи с Appium, а уже он, по этому мосту(ADB) связывается с устройством.

Malcev
Есть социальная сеть, довольно популярная в англоязычном сегменте, у которой нет WEB-представления, но есть мобильная версия. Скажем, мне от неё "кое что" нужно. Так, я имею фактически тот же набор инструментов, что и в браузере, только вместо него выступает Android.

Пока искал решение, много всего перекопал и в первую очередь, конечно же ничего не нашёл на привычном синтаксисе, кроме, разве что вот этого. Думал даже на Java или Kotlin переходить, что было бы в принципе не плохо для самообразования, но как это обычно бывает, решение нужно прямо сейчас, вокруг всё "полыхает" и всюду маячивший интерфейс Аппиума, для "обкатки" разрабатываемого софта оказался универсальным ключом, предлагая как пример готовых реализаций, так и открытый для любого языка.

В общем, попытка "зайти", сразу же показала результат, после чего были собраны основные требуемые "инструменты" этого API. Ну и не пропадать же добру. Может кому-то ещё пригодится, а этой поделке не помешает здоровая критика компетентных людей, на взгляды которых я ориентируюсь в своём поиске.

14

Re: AHK: AppiumDriver Class. Android/IOS automation.

KusochekDobra пишет:

serzh82saratov, это важное условие?

Вы про ГитХаб? Разве это не удобнее для большого кода.

По вопросам возмездной помощи пишите на E-Mail: serzh82saratov@mail.ru Telegram: https://t.me/sergiol982
Win10x64 AhkSpy, Hotkey, ClockGui

15

Re: AHK: AppiumDriver Class. Android/IOS automation.

Удобнее, но я впервые делал такое объёмное сообщение на этом форуме и решил не останавливаться перед таким незначительным ограничением и на моей памяти есть так же пример, когда кто-то оформил своё сообщение с публикацией кода на стороннем ресурсе и к нему так же пришли с вопросами, мол: - "А почему не у нас?"

Похоже, в тот момент это выглядело меньшим из зол. Честно говоря, не задумывался.

16

Re: AHK: AppiumDriver Class. Android/IOS automation.

После сталкиваешься с неудобством редактирования, и копировать такое дважды тоже не гуд.

По вопросам возмездной помощи пишите на E-Mail: serzh82saratov@mail.ru Telegram: https://t.me/sergiol982
Win10x64 AhkSpy, Hotkey, ClockGui

17 (изменено: Malcev, 2019-02-23 14:09:58)

Re: AHK: AppiumDriver Class. Android/IOS automation.

Я считаю, что код следует в первую очередь публиковать на форуме.
Так как при поиске каких-либо методов, вызовов, функций по форуму, ничего не найдется если код расположен на гитхаб.
Перевод книги Appium Essentials:
https://habr.com/ru/post/333546/

18

Re: AHK: AppiumDriver Class. Android/IOS automation.

Разбивать код на 10 постов...

По вопросам возмездной помощи пишите на E-Mail: serzh82saratov@mail.ru Telegram: https://t.me/sergiol982
Win10x64 AhkSpy, Hotkey, ClockGui

19

Re: AHK: AppiumDriver Class. Android/IOS automation.

Несколько килобайт можно и файлом прикрепить.

20

Re: AHK: AppiumDriver Class. Android/IOS automation.

Так поиском всё равно ничего не найдёшь, неудобно редактировать, иногда не скачивается, лишние гигабайты на сервере.

По вопросам возмездной помощи пишите на E-Mail: serzh82saratov@mail.ru Telegram: https://t.me/sergiol982
Win10x64 AhkSpy, Hotkey, ClockGui

21

Re: AHK: AppiumDriver Class. Android/IOS automation.

serzh82saratov, в твоём случае AhkSpy- это 4 поста.

ypppu пишет:

Несколько килобайт можно и файлом прикрепить.

А смысл?
Я имею в виду ситуацию, что мне надо посмотреть как реализовано и реализовано ли какое-либо апи на атохотки.
Гитхаб весьма плохо индексируется гуглом, поэтому в результате я ничего не найду и придется тратить время на то, что кто-то уже воплотил.

serzh82saratov пишет:

лишние гигабайты на сервере.

Про гигабайты - явное преувеличение.
Если бы с нашими админами была обратная связь, то можно было бы попробовать увеличить максимиальную длину поста:
http://punbb.informer.com/forums/topic/5532

22

Re: AHK: AppiumDriver Class. Android/IOS automation.

Malcev пишет:

в твоём случае

Когда то в 1 пост помещался, не факт что в будущем не понадобится 10. Да и 4 поста редактировать уже нереально.
На офф форуме тоже много ссылок на гитхаб. А как ещё, если несколько файлов, или код большой, и для просмотра страницы придётся каждый раз его подгружать.
Лучше как то в поиске обрабатывать содержимое по ссылкам, добавив форму для их добавления. Например если я добавляю ссылку на гитхаб, то могу добавить ссылку на raw.githubusercontent.com для использования по ней в поиске.

Malcev пишет:

Про гигабайты - явное преувеличение.

Ну если только про код, то конечно. А если принципиально добавлять всё на форум, то картинки с гифками довольно быстро скушают гигабайты.

По вопросам возмездной помощи пишите на E-Mail: serzh82saratov@mail.ru Telegram: https://t.me/sergiol982
Win10x64 AhkSpy, Hotkey, ClockGui

23

Re: AHK: AppiumDriver Class. Android/IOS automation.

А ещё, если как на забугорном, чтобы с подсветкой, возможностью разворачивать... М-м-м... Мечта!

24

Re: AHK: AppiumDriver Class. Android/IOS automation.

serzh82saratov пишет:

если несколько файлов, или код большой, и для просмотра страницы придётся каждый раз его подгружать.

В наше время сомневаюсь, что это для кого-то имеет значение.

serzh82saratov пишет:

Лучше как то в поиске обрабатывать содержимое по ссылкам, добавив форму для их добавления

Это как?

25

Re: AHK: AppiumDriver Class. Android/IOS automation.

KusochekDobra пишет:

А ещё, если как на забугорном, чтобы с подсветкой, возможностью разворачивать... М-м-м... Мечта!

Ну это уже имхо  шашечки.

26

Re: AHK: AppiumDriver Class. Android/IOS automation.

Malcev пишет:

Это как?

Так я же написал:

serzh82saratov пишет:

Например если я добавляю ссылку на гитхаб, то могу добавить ссылку на raw.githubusercontent.com для использования по ней в поиске.

При поиске, если такая ссылка прикреплена особым образом, то загружается страница и в ней тоже выполняется поиск. Или просто искать по всем ссылкам на гитхаб, перебирая все файлы в папке.

По вопросам возмездной помощи пишите на E-Mail: serzh82saratov@mail.ru Telegram: https://t.me/sergiol982
Win10x64 AhkSpy, Hotkey, ClockGui

27

Re: AHK: AppiumDriver Class. Android/IOS automation.

1) Как ты себе представляешь реализацию этого алгоритма на punbb?
2) При поиске на форуме через гугл такое будет невозможно.

28

Re: AHK: AppiumDriver Class. Android/IOS automation.

punbb я не знаю, но если с его помощью возможно индексировать определённые ссылки, то скрипт на стороне клиента заберёт их, и поочерёдно загрузит их содержимое и выполнит поиск. Гугл понятно, я имею ввиду что это будет отдельный поиск только по ссылкам на гитхаб.

По вопросам возмездной помощи пишите на E-Mail: serzh82saratov@mail.ru Telegram: https://t.me/sergiol982
Win10x64 AhkSpy, Hotkey, ClockGui

29 (изменено: Malcev, 2019-02-23 16:37:55)

Re: AHK: AppiumDriver Class. Android/IOS automation.

Ну не знаю. Выглядит сложно.
Проще в базе данных увеличить максимальную длину поста.
ypppu, может спросите админов?
Вот пример:
http://punbb.informer.com/forums/topic/5532

30

Re: AHK: AppiumDriver Class. Android/IOS automation.

Я вижу админов не чаще, чем все остальные. Если имеются вопросы/предложения - можно задать в теме  Настройка форума.

KusochekDobra пишет:

А ещё, если как на забугорном, чтобы с подсветкой, возможностью разворачивать... М-м-м... Мечта!

Подсветка кода в принципе есть в персональных настройках. Оценить не могу, т. к. обхожусь без неё.

31

Re: AHK: AppiumDriver Class. Android/IOS automation.

А что за проблемы у Malcev с поиском? Успешно ищу иногда по конкретным вызовам.
Если на репозитарий есть внешние ссылки (с тех же форумов), то код файлов в репе индексируют гугл, бинг и яндекс. Ну это там, где счет загрузок/просмотров идет на тысячи. Проверил и менее распиаренный реп - результат только в бинге, что не удивительно в свете недавних событий.
И вообще. В облачных сервисах версионирования есть собственные поиски - я проверил, работает отлично. Если мне понадобится нечто неведомое, то наш форум - последнее место где я буду искать, по объективным причинам. Считаю проблему надуманной.

32

Re: AHK: AppiumDriver Class. Android/IOS automation.

Хорошо, найдите мне скрипты на гитхабе посвященные инжекту в ahk через peixoto.dll.
Ну и просто инжекту.

33 (изменено: Malcev, 2019-02-23 19:20:54)

Re: AHK: AppiumDriver Class. Android/IOS automation.

stealzy пишет:

Если мне понадобится нечто неведомое, то наш форум - последнее место где я буду искать, по объективным причинам

Ну и поделитесь объективными причинами, если не сложно.

34

Re: AHK: AppiumDriver Class. Android/IOS automation.

1) Два клика - https://github.com/search?l=AutoHotkey& … ;type=Code
2) Конечно поделюсь: поиск следует начинать с мест, где вероятность нахождения максимальная, таким образом минимизируется время поиска. А наибольшая вероятность часто оказывается там, где наибольшее комьюнити.

35

Re: AHK: AppiumDriver Class. Android/IOS automation.

Не знал, что у зарегестрированных пользователей гитхаба есть поиск по коду.
Это плюс.
Минус по сравнению публикования кода на форуме:
Если мы находим код на гитхабе, то не всегда там будет отсылка на форум, где этот код обсуждается и не всегда на гитхабе будет опубликована лучшая по кпд и безглючности версия, которая может быть предложена в обсуждении на форуме.

stealzy пишет:

А наибольшая вероятность часто оказывается там, где наибольшее комьюнити

Далеко не всегда, так как, чем больше комьюнити, тем больше мусора, к тому же после перепрыгивания оффорума с одного домена на другой много информации похерилось.
Но это всё субъективные причины.

36

Re: AHK: AppiumDriver Class. Android/IOS automation.

Malcev пишет:

лучшая по кпд и безглючности версия

А может и устаревшая, или новая забагованая.
Системы версионирования тем и хороши, что изменения организованы и можно выбрать любую версию.

Malcev пишет:

много информации похерилось

Это да, но я смог кое-что нужное из потерянного найти потом в репах форумчан. На stakoverflow что-то проскакивает.

Malcev пишет:

тем больше мусора

Тогда интересно послушать в какой области на наших содержательнее темы чем на международных. Мне вот кроме 1С на ум ничего не пришло .

37

Re: AHK: AppiumDriver Class. Android/IOS automation.

И раз уж мы тут уже больше двадцати сообщений оффтопим (впору выносить в отдельную тему), добавлю, что у подобных сайтов разработаны собственные поисковые синтаксисы, рекомендую:
BitBucket, GitHub.

38

Re: AHK: AppiumDriver Class. Android/IOS automation.

stealzy пишет:

Системы версионирования тем и хороши, что изменения организованы и можно выбрать любую версию.

Это если автор решит вставить изменения.
Например, тут на гитхабе версия использующая ADO:
https://gist.github.com/tmplinshi/8428a280bba58d25ef0b
На оффоруме же предложен был вариант брать напрямую из памяти и этой версии на гитхабе нету:
https://www.autohotkey.com/boards/viewt … 68b#p85687

stealzy пишет:

Тогда интересно послушать в какой области на наших содержательнее темы чем на международных

Например, функция парсинга json от teadrinker не содержит возможных глюков, которая используется на оффоруме от Coco.
Дальше, если хочется поработать с памятью, то можно вбить в поиске сообщения от YMP и можно получить больше информации к анализированию, чем вбив ники других разработчиков на оффоруме.