diff --git a/.github/badges/coverage.svg b/.github/badges/coverage.svg index 12876e6..9029937 100644 --- a/.github/badges/coverage.svg +++ b/.github/badges/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 78% - 78% + 76% + 76% diff --git a/docs/assets/swiftkey_setup_1.png b/docs/assets/swiftkey_setup_1.png new file mode 100644 index 0000000..30e3aa4 Binary files /dev/null and b/docs/assets/swiftkey_setup_1.png differ diff --git a/docs/assets/swiftkey_setup_2.png b/docs/assets/swiftkey_setup_2.png new file mode 100644 index 0000000..0e3d98a Binary files /dev/null and b/docs/assets/swiftkey_setup_2.png differ diff --git a/docs/assets/swiftkey_setup_3.png b/docs/assets/swiftkey_setup_3.png new file mode 100644 index 0000000..2cda789 Binary files /dev/null and b/docs/assets/swiftkey_setup_3.png differ diff --git a/docs/assets/swiftkey_setup_4.png b/docs/assets/swiftkey_setup_4.png new file mode 100644 index 0000000..60fffa4 Binary files /dev/null and b/docs/assets/swiftkey_setup_4.png differ diff --git a/docs/emu_setup.md b/docs/emu_setup.md index 2fbafbc..8c6c70d 100644 --- a/docs/emu_setup.md +++ b/docs/emu_setup.md @@ -103,6 +103,40 @@ Make sure to disable the clipboard : !!! failure "Layout" For now, the only layout supported is `english US`. Make sure this is the layout GBoard is using. +### Preparing Swiftkey + +Swiftkey keyboard isn't installed on the emulator by default : you need to install it first. + +!!! note + If you want to run the tests in parallel on several emulators, you need to repeat these steps for each emulator. + +Start the emulator, then go to Google, and paste [this link](https://play.google.com/store/apps/details?id=com.touchtype.swiftkey&hl=en_US&gl=US) to install Swiftkey. + +!!! tip + If the clipboard isn't shared with the emulator, open a terminal and run : + + ```bash + adb shell input text "https://play.google.com/store/apps/details?id=com.touchtype.swiftkey&hl=en_US&gl=US" + ``` + +Install the keyboard on your emulator : + +![](assets/swiftkey_setup_1.png){ width="300" } + +Open the app, follow the instructions to activate the keyboard. + +--- + +By default, Swiftkey has the clipboard enabled, and it may interfere with the layout detection. You can disable the clipboard. First, access the clipboard settings : + +![](assets/swiftkey_setup_2.png){ width="300" } + +![](assets/swiftkey_setup_3.png){ width="300" } + +And disable the clipboard suggestions : + +![](assets/swiftkey_setup_4.png){ width="300" } + ## Setting up iOS emulator ### Creating the emulator diff --git a/kebbie/cmd.py b/kebbie/cmd.py index 111c415..4ea72cf 100644 --- a/kebbie/cmd.py +++ b/kebbie/cmd.py @@ -28,7 +28,7 @@ def instantiate_correctors( Returns: The list of created Correctors. """ - if keyboard in ["gboard", "tappa"]: + if keyboard in ["gboard", "tappa", "swiftkey"]: # Android keyboards return [ EmulatorCorrector( @@ -68,7 +68,7 @@ def common_args(parser: argparse.ArgumentParser): dest="keyboard", type=str, required=True, - choices=["gboard", "ios", "kbkitpro", "kbkitoss", "tappa", "fleksy"], + choices=["gboard", "ios", "kbkitpro", "kbkitoss", "tappa", "fleksy", "swiftkey"], help="Which keyboard, to be tested, is currently installed on the emulator.", ) diff --git a/kebbie/emulator.py b/kebbie/emulator.py index 33701dd..e696993 100644 --- a/kebbie/emulator.py +++ b/kebbie/emulator.py @@ -32,6 +32,12 @@ FLEKSY = "fleksy" KBKITPRO = "kbkitpro" KBKITOSS = "kbkitoss" +SWIFTKEY = "swiftkey" +KEYBOARD_PACKAGE = { + GBOARD: "com.google.android.inputmethod.latin", + SWIFTKEY: "com.touchtype.swiftkey", + TAPPA: "com.tappa.keyboard", +} ANDROID_CAPABILITIES = { "platformName": "android", "automationName": "UiAutomator2", @@ -98,10 +104,13 @@ "Keyboard Type - emojis": "smiley", "Search": "enter", "return": "enter", + "Enter": "enter", "Symbol keyboard": "numbers", "Symbols": "numbers", + "Symbols and numbers": "numbers", "Keyboard Type - numeric": "numbers", "Voice input": "mic", + ",, alternatives available, Voice typing, long press to activate": "mic", "Close features menu": "magic", "Open features menu": "magic", "underline": "_", @@ -123,7 +132,35 @@ "Digit keyboard": "numbers", "More symbols": "shift", "Keyboard Type - symbolic": "shift", + "Double tap for uppercase": "shift", + "Double tap for caps lock": "shift", + "capital Q": "Q", + "capital W": "W", + "capital E": "E", + "capital R": "R", + "capital T": "T", + "capital Y": "Y", + "capital U": "U", + "capital I": "I", "Capital I": "I", + "capital O": "O", + "capital P": "P", + "capital A": "A", + "capital S": "S", + "capital D": "D", + "capital F": "F", + "capital G": "G", + "capital H": "H", + "capital J": "J", + "capital K": "K", + "capital L": "L", + "capital Z": "Z", + "capital X": "X", + "capital C": "C", + "capital V": "V", + "capital B": "B", + "capital N": "N", + "capital M": "M", } FLEKSY_LAYOUT = { "keyboard_frame": [0, 517, 393, 266], # Only the keyboard frame is defined as absolute position @@ -252,7 +289,7 @@ class Emulator: ValueError: Error raised if the given platform doesn't exist. """ - def __init__( + def __init__( # noqa: C901 self, platform: str, keyboard: str, @@ -293,6 +330,10 @@ def __init__( self.last_char_is_space = False self.last_char_is_eos = False + # Set the keyboard as default + if self.platform == ANDROID: + self.select_keyboard(keyboard) + # Get the right layout if self.keyboard == GBOARD: self.detected = GboardLayoutDetector(self.driver, self._tap) @@ -312,10 +353,13 @@ def __init__( elif self.keyboard == KBKITOSS: self.detected = KbkitossLayoutDetector(self.driver, self._tap) self.layout = self.detected.layout + elif self.keyboard == SWIFTKEY: + self.detected = SwiftkeyLayoutDetector(self.driver, self._tap) + self.layout = self.detected.layout else: raise ValueError( - f"Unknown keyboard : {self.keyboard}. Please specify `{GBOARD}`, `{TAPPA}`, `{FLEKSY}`, `{KBKITPRO}`, " - f"`{KBKITOSS}` or `{IOS}`." + f"Unknown keyboard : {self.keyboard}. Please specify `{GBOARD}`, `{TAPPA}`, `{FLEKSY}`, " + f"`{SWIFTKEY}`, `{KBKITPRO}`, `{KBKITOSS}` or `{IOS}`." ) self.typing_field.clear() @@ -352,6 +396,33 @@ def get_android_devices() -> List[str]: devices = [d.split()[0] for d in devices if not (d.startswith("List of devices attached") or len(d) == 0)] return devices + def select_keyboard(self, keyboard): + """Searches the IME of the desired keyboard and selects it, only for Android. + + Args: + keyboard (str): Keyboard to search. + """ + if keyboard not in KEYBOARD_PACKAGE: + print( + f"Warning ! {keyboard}'s IME isn't provided (in `KEYBOARD_PACKAGE`), can't automatically select the " + "keyboard." + ) + return + + ime_list = subprocess.check_output(["adb", "shell", "ime", "list", "-s"], universal_newlines=True) + ime_name = None + for ime in ime_list.strip().split("\n"): + if KEYBOARD_PACKAGE[keyboard] in ime: + ime_name = ime + break + if ime_name: + subprocess.run( + ["adb", "shell", "settings", "put", "secure", "show_ime_with_hard_keyboard", "1"], + stdout=subprocess.PIPE, + ) + subprocess.run(["adb", "shell", "ime", "enable", ime_name], stdout=subprocess.PIPE) + subprocess.run(["adb", "shell", "ime", "set", ime_name], stdout=subprocess.PIPE) + def get_ios_devices() -> List[Tuple[str, str]]: """Static method that uses the `xcrun simctl` command to retrieve the list of booted devices. @@ -453,6 +524,9 @@ def type_characters(self, characters: str): # noqa: C901 if self.kb_is_upper: # If the keyboard is in uppercase mode, change it to lowercase self._tap(self.layout["uppercase"]["shift"]) + if self.keyboard == SWIFTKEY: + # Swiftkey needs double tap, otherwise we are capslocking + self._tap(self.layout["uppercase"]["shift"]) self._tap(self.layout["lowercase"][c]) elif c in self.layout["uppercase"]: # The character is an uppercase character @@ -470,9 +544,9 @@ def type_characters(self, characters: str): # noqa: C901 self._tap(self.layout["lowercase"]["numbers"]) self._tap(self.layout["numbers"][c]) - if c != "'" or self.keyboard == GBOARD: + if c != "'" or self.keyboard in [GBOARD, SWIFTKEY]: # For some reason, when `'` is typed, the keyboard automatically goes back - # to lowercase, so no need to re-tap the button (unless the keyboard is GBoard). + # to lowercase, so no need to re-tap the button (unless the keyboard is GBoard / Swiftkey). # In all other cases, switch back to letters keyboard self._tap(self.layout["numbers"]["letters"]) else: @@ -866,7 +940,7 @@ class GboardLayoutDetector(LayoutDetector): def __init__(self, *args, **kwargs): super().__init__( *args, - xpath_root="./*/*[@package='com.google.android.inputmethod.latin']", + xpath_root=f"./*/*[@package='{KEYBOARD_PACKAGE[GBOARD]}']", xpath_keys=".//*[@resource-id][@content-desc]", **kwargs, ) @@ -1009,6 +1083,40 @@ def get_suggestions(self) -> List[str]: return suggestions +class SwiftkeyLayoutDetector(LayoutDetector): + """Layout detector for the Swiftkey keyboard. See `LayoutDetector` for more + information. + """ + + def __init__(self, *args, **kwargs): + super().__init__( + *args, + xpath_root=f"./*/*[@package='{KEYBOARD_PACKAGE[SWIFTKEY]}']", + xpath_keys=".//*[@class='android.view.View'][@content-desc]", + **kwargs, + ) + + def get_suggestions(self) -> List[str]: + """Method to retrieve the keyboard suggestions from the XML tree. + + Returns: + List of suggestions from the keyboard. + """ + suggestions = [] + + # Get the raw content as text, weed out useless elements + for data in self.driver.page_source.split(" List[str]: suggestions = [] # Get the raw content as text, weed out useless elements - section = self.driver.page_source.split("com.tappa.keyboard:id/toolbar")[1].split( + section = self.driver.page_source.split(f"{KEYBOARD_PACKAGE[TAPPA]}:id/toolbar")[1].split( "" )[0] diff --git a/tests/test_emulator.py b/tests/test_emulator.py index 4b15bef..eda5d62 100644 --- a/tests/test_emulator.py +++ b/tests/test_emulator.py @@ -23,11 +23,13 @@ class SubprocessResult: def test_get_android_devices(monkeypatch): def android_subprocess(*args, **kwargs): return SubprocessResult( - DummyStdout("""List of devices attached + DummyStdout( + """List of devices attached emulator-5554 device emulator-5558 device -""") +""" + ) ) monkeypatch.setattr(subprocess, "run", android_subprocess) @@ -42,7 +44,8 @@ def android_subprocess(*args, **kwargs): def test_get_ios_devices(monkeypatch): def ios_subprocess(*args, **kwargs): return SubprocessResult( - DummyStdout("""== Devices == + DummyStdout( + """== Devices == -- iOS 14.4 -- iPhone 12 mini (8A192CB8-A72C-4BBA-9A98-2476E66ABEF8) (Shutdown) (unavailable) iPhone 12 (C0E1F6AB-FDA5-4953-BB22-7CDB09D3B303) (Shutdown) (unavailable) @@ -71,7 +74,8 @@ def ios_subprocess(*args, **kwargs): iPad mini (6th generation) (EC7F8EF2-D5A6-437B-9531-E2DBE924FB5A) (Shutdown) (unavailable) iPad Pro (11-inch) (4th generation) (49F56D2F-5722-41CD-9C11-D4979084DA3E) (Shutdown) (unavailable) iPad Pro (12.9-inch) (6th generation) (08E5485D-D293-4B7B-9831-F29D55EDA053) (Shutdown) (unavailable) -""") +""" + ) ) monkeypatch.setattr(subprocess, "run", ios_subprocess)