Skip to content

Commit 76c7f2f

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

File tree

2 files changed

+71
-37
lines changed

2 files changed

+71
-37
lines changed

autotests/appiumtests/clipboardtest.py

Lines changed: 6 additions & 0 deletions
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

@@ -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: 65 additions & 37 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,28 @@ 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+
667+
return json.dumps({'value': None}), 200, {'content-type': 'application/json'}
645668

646669

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

785808
blob = json.loads(request.data)
786809
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()
810+
data = cast(str, get_clipboard(contentType))
806811

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

@@ -1065,6 +1070,29 @@ def char_to_keyval(ch):
10651070
return keyval
10661071

10671072

1073+
def get_clipboard(content_type):
1074+
# NOTE: need a window because on wayland we must be the active window to manipulate the clipboard (currently anyway)
1075+
window = Gtk.Window()
1076+
window.set_default_size(20, 20)
1077+
window.show()
1078+
display = window.get_display()
1079+
clipboard = Gtk.Clipboard.get_for_display(display, Gdk.SELECTION_CLIPBOARD)
1080+
1081+
spin_glib_main_context()
1082+
1083+
data = None
1084+
if content_type == 'plaintext':
1085+
data = clipboard.wait_for_text()
1086+
else:
1087+
raise 'content type not currently supported'
1088+
1089+
window.close()
1090+
1091+
spin_glib_main_context()
1092+
1093+
return data
1094+
1095+
10681096
def spin_glib_main_context(repeat: int = 4):
10691097
context = GLib.MainContext.default()
10701098
for _ in range(repeat):

0 commit comments

Comments
 (0)