개념
Droidline의 모든 명령에 깔린 개념을 한곳에서 설명합니다. 트리로 보는 화면, 선택자, 자동 대기, 조건 확인, 에러, 여러 폰, 네트워크를 끊는 명령, 그리고 명령이 두 번 실행되지 않는 이유를 다룹니다.
번역된 페이지입니다. 영어 문서와 내용이 다르면 영어 문서가 기준입니다. English
이 페이지는 Droidline이 폰을 어떻게 바라보는지 설명합니다. 시작할 때 전부 알아야 하는 것은 아니지만, 각 절은 처음 일주일 안에 흔히 부딪히는 질문에 답합니다. 예제는 Python과 Node.js로 되어 있고, 모든 인터페이스의 모든 명령은 명령어 레퍼런스에 있습니다.
트리로 보는 화면
안드로이드는 화면에 있는 것을 요소 트리로 표현합니다. 시각장애인을 위한 스크린 리더가 쓰는 바로 그 트리입니다. 화면은 큰 상자이고, 그 안에 툴바나 목록 같은 작은 상자가 있으며, 다시 그 안에 버튼, 텍스트, 이미지가 있습니다.
dump()는 그 트리를 돌려줍니다. 각 요소에는 다음 속성이 있습니다.
{
"text": "Log in",
"id": "com.example:id/login",
"desc": "",
"class": "android.widget.Button",
"bounds": [60, 1000, 1020, 1140],
"clickable": true,
"enabled": true,
"checked": false,
"children": []
}
| 속성 | 의미 |
|---|---|
text | 요소가 보여 주는 글자. |
id | 앱 개발자가 붙인 리소스 ID. 없는 요소도 많습니다. |
desc | 스크린 리더를 위해 쓴 콘텐츠 설명(content description). 아이콘 버튼에는 이것만 있는 경우가 많습니다. |
class | 요소의 종류. 예를 들면 android.widget.Button. |
bounds | 위치. 화면 픽셀 단위의 [left, top, right, bottom]. |
clickable, enabled, checked, selected | 요소의 상태. |
d.dump("screen.json") # 트리를 PC의 파일로 저장
tree = d.dump() # 또는 dict로 돌려받음await d.dump("screen.json"); // 트리를 PC의 파일로 저장
const tree = await d.dump(); // 또는 객체로 돌려받음dump를 읽고 알맞은 요소를 고르는 과정은 요소 찾기에서 차근차근 설명합니다.
선택자: 속성과 값
요소 하나를 다루는 명령은 먼저 인자 두 개를 받습니다. 비교할 속성(by)과 찾을 값입니다. 둘을 합쳐 선택자라고 합니다.
by | 일치 조건 | 예 |
|---|---|---|
text | text 완전 일치 | touch("text", "Log in") |
textContains | text의 일부 | touch("textContains", "Log") |
id | id. 이름만 쓰면 패키지와 상관없이 일치합니다. "login"은 com.example:id/login과 일치 | touch("id", "login") |
desc | desc 완전 일치 | touch("desc", "Search") |
descContains | desc의 일부 | touch("descContains", "Sear") |
class | class, 보통 nth와 함께 사용 | touch("class", "android.widget.Button", nth=2) |
여러 요소가 일치하면 nth로 하나를 고릅니다. 화면에 나오는 순서대로 0부터 셉니다. touchById, touchByText, touchByDesc는 가장 많이 쓰는 세 속성의 축약형입니다.
d.touch("id", "login")
d.touchById("login") # 위와 같음
d.touch("class", "android.widget.CheckBox", nth=1) # 두 번째 체크박스await d.touch("id", "login");
await d.touchById("login"); // 위와 같음
await d.touch("class", "android.widget.CheckBox", { nth: 1 }); // 두 번째 체크박스어떤 속성을 고를까요? id는 언어가 바뀌어도, 앱이 웬만큼 업데이트되어도 그대로이므로 있다면 id를 우선 쓰세요. text는 읽기 쉽지만 폰의 언어에 따라 바뀝니다. 아이콘 버튼에는 보통 desc가 있습니다.
자동 대기
폰은 예측할 수 없게 느릴 때가 있습니다. 앱이 시작되는 데 잠깐 걸리기도 하고, 목록을 네트워크에서 불러오기도 합니다. Droidline이 이 문제를 대신 처리합니다. touch, long_touch, input, clear, wait는 대상 요소가 나타날 때까지 기다립니다. 기본값은 10초입니다. 이 명령 앞에 직접 sleep을 넣지 마세요.
d.touch("text", "Done") # 최대 10초 기다림
d.touch("text", "Done", timeout=30) # 최대 30초 기다림
d.wait("text", "Upload complete", 120) # 탭하지 않고 기다리기만 함
d.wait_gone("text", "Loading") # 무언가 사라질 때까지 기다림await d.touch("text", "Done"); // 최대 10초 기다림
await d.touch("text", "Done", { timeout: 30 }); // 최대 30초 기다림
await d.wait("text", "Upload complete", 120); // 탭하지 않고 기다리기만 함
await d.waitGone("text", "Loading"); // 무언가 사라질 때까지 기다림기다리는 일은 폰에서 일어나므로, 기다리는 동안 네트워크에 부담이 가지 않습니다.
탭이 전달되는 방식
touch는 요소를 찾으면 탭하는 방법 세 가지를 차례로 시도하고, 어느 방법이 통했는지 응답의 via 항목으로 알려 줍니다.
node: 요소 자체의 클릭 동작. 가장 깔끔한 방법입니다.parent: 가장 가까운 클릭 가능한 상위 요소. 클릭 가능한 행 안에 있는 라벨에 해당합니다.gesture: 요소bounds의 중심을 실제로 탭합니다.
input도 같은 방식입니다. 접근성으로 텍스트를 설정하고(via: "set_text"), 이 방법을 무시하는 입력란에서는 Droidline 키보드로 타이핑하는 방식으로 넘어갑니다(via: "ime").
바로 응답하는 조건 확인
어떤 명령은 질문입니다. exists, get_text, checked, enabled, selected, count, in_app, keyboard_shown, last_toast, color, installed, 그리고 battery 같은 기기 상태 확인 명령이 그렇습니다. 이 명령들은 기다리지도 실패하지도 않습니다. 요소가 없으면 에러 대신 false, "", 0, -1을 돌려주므로 if에 그대로 넣을 수 있습니다.
if d.exists("text", "Close ad"):
d.touch("text", "Close ad")
if d.battery()["level"] < 20:
print("charge me")if (await d.exists("text", "Close ad")) {
await d.touch("text", "Close ad");
}
if ((await d.battery()).level < 20) {
console.log("charge me");
}여러 화면 중 하나를 기다리기
조건 확인 중에서 기다리는 것은 which 하나뿐입니다. 선택자 목록을 넘기면 가장 먼저 나타난 선택자의 위치를 돌려주고, 타임아웃 전에 아무것도 나타나지 않으면 -1을 돌려줍니다. 결과가 여러 갈래로 나뉠 수 있는 단계를 다루는 가장 깔끔한 방법입니다.
state = d.which([("text", "Log in"), ("id", "main_tab")], timeout=15)
if state == 0: # 로그인 화면이 먼저 나타남
d.input("id", "email", "me@example.com")
elif state == -1: # 둘 다 나타나지 않음
d.screenshot("unknown.png")const state = await d.which([["text", "Log in"], ["id", "main_tab"]], { timeout: 15 });
if (state === 0) { // 로그인 화면이 먼저 나타남
await d.input("id", "email", "me@example.com");
} else if (state === -1) { // 둘 다 나타나지 않음
await d.screenshot("unknown.png");
}에러
할 일을 해내지 못한 명령은 에러로 실패합니다. 모든 에러에는 code, 읽기 쉬운 메시지, 그리고 다시 시도하면 도움이 될지 알려 주는 retryable이 있습니다.
NOT_FOUND: Could not find text 'Log in' within 10s. Current screen: com.example / .MainActivity
| 인터페이스 | 실패했을 때 |
|---|---|
| Python | 예외. DroidlineError의 하위 클래스인 NotFoundError, DeviceOfflineError 등 |
| Node.js | code, retryable, data가 있는 DroidlineError |
| CLI | 표준 에러로 출력되는 메시지와 종료 상태 1 |
| HTTP | 해당 코드에 정해진 HTTP 상태와 같은 JSON 본문 |
| 소켓 | "ok": false가 담긴 응답 줄 |
from droidline import NotFoundError, DroidlineError
try:
d.touch("text", "Log in", timeout=5)
except NotFoundError:
d.back()
except DroidlineError as e:
print(e.code, e.retryable, e)import { DroidlineError } from "droidline";
try {
await d.touch("text", "Log in", { timeout: 5 });
} catch (e) {
if (!(e instanceof DroidlineError)) throw e;
if (e.code === "NOT_FOUND") await d.back();
else console.log(e.code, e.retryable, e.message);
}메시지는 서버 언어로 나옵니다. config.toml의 lang 설정이나 시스템 언어에 따라 영어, 한국어, 중국어 중 하나입니다. 코드는 언제나 같습니다. 전체 목록은 에러 코드에 있습니다.
폰 한 대, 여러 대
온라인인 폰이 딱 한 대라면 폰을 지정할 필요가 없습니다. 두 대 이상이면 쓸 폰을 지정해야 하고, 그러지 않으면 호출이 DEVICE_AMBIGUOUS로 실패합니다.
from droidline import Droidline
dl = Droidline()
for info in dl.devices():
print(info["name"], info["online"], info.get("route"))
a = dl.device("shelf-01")
b = dl.device("shelf-02")
a.launch("com.android.chrome")import { Droidline } from "droidline";
const dl = new Droidline();
for (const info of await dl.devices()) {
console.log(info.name, info.online, info.route);
}
const a = dl.device("shelf-01");
const b = dl.device("shelf-02");
await a.launch("com.android.chrome");droidline --device shelf-01 launch com.android.chrome모든 인터페이스는 DROIDLINE_DEVICE 환경 변수도 기본 폰으로 읽습니다. 폰에 짧은 이름을 붙이려면 droidline rename <id> <name>을 씁니다.
같은 폰에 보낸 명령은 보낸 순서대로 하나씩 실행됩니다. 서로 다른 폰에 보낸 명령은 동시에 실행됩니다. 선반 가득한 폰을 병렬로 다루는 방법은 여러 폰에서 설명합니다.
폰이 오프라인이면 명령은 폰이 돌아오기를 최대 30초(config.toml의 offline_wait) 기다린 뒤 DEVICE_OFFLINE으로 실패합니다.
폰 자신의 연결을 끊는 명령
data(False), wifi(False), airplane(True)는 폰이 PC와 통신할 때 쓰는 네트워크를 없앱니다. Droidline은 이것을 두 가지 방식으로 다룹니다.
기본적으로 이 명령들은 실제로 실행하기 전에 {"accepted": true}로 먼저 응답하고, 최종 결과는 폰이 다시 접속한 뒤에 도착합니다. 최종 결과를 일반 반환값으로 받으려면 wait=True(CLI: --wait)를 넘기세요.
d.airplane(True, wait=True)await d.airplane(true, { wait: true });droidline airplane true --wait폰이 오프라인인 동안 일어나야 하는 일은 모두 하나의 batch에 넣습니다. 폰이 목록 전체를 혼자 실행하고, 다시 온라인이 되면 결과를 알려 줍니다.
d.batch([("airplane", True), ("sleep", 3000), ("airplane", False)], wait=True)await d.batch([["airplane", true], ["sleep", 3000], ["airplane", false]], { wait: true });droidline batch '[["airplane",true],["sleep",3000],["airplane",false]]' --wait새 모바일 IP 주소를 받을 때 보통 이렇게 합니다. sleep은 batch 안에서만 쓸 수 있습니다.
두 번 실행되지 않음
모바일 데이터에서는 PC와 폰 사이의 연결이 언제든 끊길 수 있고, 명령을 실행하는 도중에도 끊길 수 있습니다. 그래도 Droidline은 명령이 정확히 한 번만 실행되게 합니다.
- 모든 명령에는 ID가 붙습니다.
- 폰은 PC가 받았다고 확인할 때까지 각 응답을 보관합니다.
- 연결이 끊기면 PC는 폰이 돌아왔을 때 같은 ID로 명령을 다시 보냅니다.
- 폰은 그 ID를 알아보고, 명령을 다시 실행하는 대신 보관해 둔 응답을 보냅니다.
그사이 폰의 Droidline 앱이 재시작됐다면 기억하던 ID가 사라집니다. 이때 명령은 AGENT_RESTARTED로 실패하므로, 다시 실행해도 안전한지는 내 코드에서 판단할 수 있습니다.