2
2
# SPDX-FileCopyrightText: 2021-2023 Harald Sitter <sitter@kde.org>
3
3
4
4
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
12
5
import json
13
- import sys
6
+ import logging
14
7
import os
15
8
import signal
16
9
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
18
17
18
+ import gi
19
+ import numpy as np
19
20
import pyatspi
21
+ from flask import Flask , jsonify , request
20
22
from lxml import etree
23
+ from werkzeug .exceptions import HTTPException
24
+
25
+ from app_roles import ROLE_NAMES
21
26
22
- import gi
23
- from gi .repository import GLib
24
- from gi .repository import Gio
25
27
gi .require_version ('Gdk' , '3.0' )
26
- from gi .repository import Gdk
27
28
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
31
30
32
31
# Exposes AT-SPI as a webdriver. This is written in python because C sucks and pyatspi is a first class binding so
33
32
# we lose nothing but gain the reduced sucking of python.
40
39
EVENTLOOP_TIME = 0.1
41
40
EVENTLOOP_TIME_LONG = 0.5
42
41
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 )
44
45
45
46
# 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
46
47
# 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
50
# Using flask because I know nothing about writing REST in python and it seemed the most straight-forward framework.
50
51
app = Flask (__name__ )
51
52
53
+
52
54
@app .errorhandler (Exception )
53
55
def unknown_error (e ):
54
56
if isinstance (e , HTTPException ):
@@ -641,7 +643,33 @@ def session_element_value(session_id, element_id):
641
643
break
642
644
if not processed :
643
645
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' }
645
673
646
674
647
675
@app .route ('/session/<session_id>/element/<element_id>/clear' , methods = ['POST' ])
@@ -784,25 +812,7 @@ def session_appium_device_get_clipboard(session_id):
784
812
785
813
blob = json .loads (request .data )
786
814
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 ))
806
816
807
817
return json .dumps ({'value' : base64 .b64encode (data .encode ('utf-8' )).decode ('utf-8' )}), 200 , {'content-type' : 'application/json' }
808
818
@@ -817,22 +827,7 @@ def session_appium_device_set_clipboard(session_id):
817
827
contentType = blob ['contentType' ]
818
828
content = blob ['content' ]
819
829
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 )
836
831
837
832
return json .dumps ({'value' : None }), 200 , {'content-type' : 'application/json' }
838
833
@@ -1065,6 +1060,48 @@ def char_to_keyval(ch):
1065
1060
return keyval
1066
1061
1067
1062
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
+
1068
1105
def spin_glib_main_context (repeat : int = 4 ):
1069
1106
context = GLib .MainContext .default ()
1070
1107
for _ in range (repeat ):
0 commit comments