Skip to content

Commit 728f64a

Browse files
committed
Implement endpoint for execute_script
Similar to appium/python-client#998
1 parent 163a38c commit 728f64a

File tree

4 files changed

+102
-60
lines changed

4 files changed

+102
-60
lines changed

autotests/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ set_tests_properties(falsetest PROPERTIES
1616
WILL_FAIL TRUE
1717
ENVIRONMENT "TEST_WITH_KWIN_WAYLAND=0")
1818

19+
add_test(
20+
NAME clipboardtest
21+
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/clipboardtest.py
22+
)
23+
1924
add_test(
2025
NAME screenshottest
2126
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/screenshottest.py

autotests/appiumtests/CMakeLists.txt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,6 @@ if(CMAKE_SYSTEM_NAME MATCHES "Linux")
2525
)
2626

2727

28-
add_test(
29-
NAME clipboardtest
30-
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/clipboardtest.py
31-
)
32-
set_tests_properties(clipboardtest PROPERTIES TIMEOUT 30)
33-
3428
add_test(
3529
NAME shiftsynthesizertest
3630
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/shiftsynthesizertest.rb

autotests/appiumtests/clipboardtest.py renamed to autotests/clipboardtest.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
# SPDX-License-Identifier: MIT
44
# SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
55

6+
import base64
67
import unittest
8+
79
from appium import webdriver
810
from appium.options.common.base import AppiumOptions
911

@@ -14,7 +16,7 @@ class SimpleCalculatorTests(unittest.TestCase):
1416
def setUpClass(self):
1517
options = AppiumOptions()
1618
# unused actually but need one so the driver is happy
17-
options.set_capability("app", "org.kde.kcalc.desktop")
19+
options.set_capability("app", "Root")
1820
self.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
1921

2022
@classmethod
@@ -31,6 +33,10 @@ def test_initialize(self):
3133
text = self.driver.get_clipboard_text()
3234
self.assertEqual(text, "qwer")
3335

36+
base64_str = self.driver.execute_script("mobile: getClipboard")
37+
text = base64.b64decode(base64_str).decode('utf-8')
38+
self.assertEqual(text, "qwer")
39+
3440

3541
if __name__ == '__main__':
3642
unittest.main()

selenium-webdriver-at-spi.py

Lines changed: 90 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,31 @@
22
# SPDX-FileCopyrightText: 2021-2023 Harald Sitter <sitter@kde.org>
33

44
import base64
5-
from datetime import datetime, timedelta
6-
import numpy as np
7-
import tempfile
8-
import time
9-
import traceback
10-
from flask import Flask, request, jsonify
11-
import uuid
125
import json
13-
import sys
6+
import logging
147
import os
158
import signal
169
import subprocess
17-
from werkzeug.exceptions import HTTPException
10+
import sys
11+
import tempfile
12+
import time
13+
import traceback
14+
import uuid
15+
from datetime import datetime, timedelta
16+
from typing import cast
1817

18+
import gi
19+
import numpy as np
1920
import pyatspi
21+
from flask import Flask, jsonify, request
2022
from lxml import etree
23+
from werkzeug.exceptions import HTTPException
24+
25+
from app_roles import ROLE_NAMES
2126

22-
import gi
23-
from gi.repository import GLib
24-
from gi.repository import Gio
2527
gi.require_version('Gdk', '3.0')
26-
from gi.repository import Gdk
2728
gi.require_version('Gtk', '3.0')
28-
from gi.repository import Gtk
29-
30-
from app_roles import ROLE_NAMES
29+
from gi.repository import Gdk, Gio, GLib, Gtk
3130

3231
# Exposes AT-SPI as a webdriver. This is written in python because C sucks and pyatspi is a first class binding so
3332
# we lose nothing but gain the reduced sucking of python.
@@ -40,7 +39,9 @@
4039
EVENTLOOP_TIME = 0.1
4140
EVENTLOOP_TIME_LONG = 0.5
4241
sys.stdout = sys.stderr
43-
sessions = {} # global dict of open sessions
42+
sessions = {} # global dict of open sessions
43+
44+
logger = logging.Logger("selenium-webdriver-at-spi", logging.INFO)
4445

4546
# Give the GUI enough time to react. tests run on the CI won't always be responsive in the tight schedule established by at-spi2 (800ms) and run risk
4647
# of timing out on (e.g.) click events. The second value is the timeout for app startup, we keep that the same as upstream.
@@ -49,6 +50,7 @@
4950
# Using flask because I know nothing about writing REST in python and it seemed the most straight-forward framework.
5051
app = Flask(__name__)
5152

53+
5254
@app.errorhandler(Exception)
5355
def unknown_error(e):
5456
if isinstance(e, HTTPException):
@@ -641,7 +643,33 @@ def session_element_value(session_id, element_id):
641643
break
642644
if not processed:
643645
raise RuntimeError("element's actions list didn't contain SetFocus. The element may be malformed")
644-
return json.dumps({'value': None}), 200, {'content-type': 'application/json'}
646+
647+
648+
@app.route('/session/<session_id>/execute/sync', methods=['POST'])
649+
def session_execute(session_id):
650+
session = sessions[session_id]
651+
if not session:
652+
return json.dumps({'value': {'error': 'no such window'}}), 404, {'content-type': 'application/json'}
653+
654+
blob = json.loads(request.data)
655+
script = cast(str, blob['script'])
656+
args = blob['args']
657+
658+
logger.info(script)
659+
logger.info(args)
660+
661+
match script:
662+
case "mobile: getClipboard":
663+
content_type = cast(str, args['contentType'] if 'contentType' in args else 'plaintext')
664+
data = cast(str, get_clipboard(content_type))
665+
return json.dumps({'value': base64.b64encode(data.encode('utf-8')).decode('utf-8')}), 200, {'content-type': 'application/json'}
666+
case "mobile: setClipboard":
667+
content = args['content']
668+
content_type = cast(str, args['contentType'])
669+
set_clipboard(content, content_type)
670+
return json.dumps({'value': None}), 200, {'content-type': 'application/json'}
671+
672+
return json.dumps({'value': {'error': 'no such command'}}), 404, {'content-type': 'application/json'}
645673

646674

647675
@app.route('/session/<session_id>/element/<element_id>/clear', methods=['POST'])
@@ -784,25 +812,7 @@ def session_appium_device_get_clipboard(session_id):
784812

785813
blob = json.loads(request.data)
786814
contentType = blob['contentType']
787-
788-
# NOTE: need a window because on wayland we must be the active window to manipulate the clipboard (currently anyway)
789-
window = Gtk.Window()
790-
window.set_default_size(20, 20)
791-
window.show()
792-
display = window.get_display()
793-
clipboard = Gtk.Clipboard.get_for_display(display, Gdk.SELECTION_CLIPBOARD)
794-
795-
spin_glib_main_context()
796-
797-
data = None
798-
if contentType == 'plaintext':
799-
data = clipboard.wait_for_text()
800-
else:
801-
raise 'content type not currently supported'
802-
803-
window.close()
804-
805-
spin_glib_main_context()
815+
data = cast(str, get_clipboard(contentType))
806816

807817
return json.dumps({'value': base64.b64encode(data.encode('utf-8')).decode('utf-8')}), 200, {'content-type': 'application/json'}
808818

@@ -817,22 +827,7 @@ def session_appium_device_set_clipboard(session_id):
817827
contentType = blob['contentType']
818828
content = blob['content']
819829

820-
# NOTE: need a window because on wayland we must be the active window to manipulate the clipboard (currently anyway)
821-
window = Gtk.Window()
822-
window.set_default_size(20, 20)
823-
display = window.get_display()
824-
clipboard = Gtk.Clipboard.get_for_display(display, Gdk.SELECTION_CLIPBOARD)
825-
826-
if contentType == 'plaintext':
827-
clipboard.set_text(base64.b64decode(content).decode('utf-8'), -1)
828-
else:
829-
raise 'content type not currently supported'
830-
831-
spin_glib_main_context()
832-
833-
window.close()
834-
835-
spin_glib_main_context()
830+
set_clipboard(content, contentType)
836831

837832
return json.dumps({'value': None}), 200, {'content-type': 'application/json'}
838833

@@ -1065,6 +1060,48 @@ def char_to_keyval(ch):
10651060
return keyval
10661061

10671062

1063+
def get_clipboard(content_type):
1064+
# NOTE: need a window because on wayland we must be the active window to manipulate the clipboard (currently anyway)
1065+
window = Gtk.Window()
1066+
window.set_default_size(20, 20)
1067+
window.show()
1068+
display = window.get_display()
1069+
clipboard = Gtk.Clipboard.get_for_display(display, Gdk.SELECTION_CLIPBOARD)
1070+
1071+
spin_glib_main_context()
1072+
1073+
data = None
1074+
if content_type == 'plaintext':
1075+
data = clipboard.wait_for_text()
1076+
else:
1077+
raise 'content type not currently supported'
1078+
1079+
window.close()
1080+
1081+
spin_glib_main_context()
1082+
1083+
return data
1084+
1085+
1086+
def set_clipboard(content, content_type):
1087+
# NOTE: need a window because on wayland we must be the active window to manipulate the clipboard (currently anyway)
1088+
window = Gtk.Window()
1089+
window.set_default_size(20, 20)
1090+
display = window.get_display()
1091+
clipboard = Gtk.Clipboard.get_for_display(display, Gdk.SELECTION_CLIPBOARD)
1092+
1093+
if content_type == 'plaintext':
1094+
clipboard.set_text(base64.b64decode(content).decode('utf-8'), -1)
1095+
else:
1096+
raise 'content type not currently supported'
1097+
1098+
spin_glib_main_context()
1099+
1100+
window.close()
1101+
1102+
spin_glib_main_context()
1103+
1104+
10681105
def spin_glib_main_context(repeat: int = 4):
10691106
context = GLib.MainContext.default()
10701107
for _ in range(repeat):

0 commit comments

Comments
 (0)