mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(driver): add escape to the pressKey surface
escape is a key a spec has real use for and no platform could send it. android maps it to KEYCODE_ESCAPE, the ios companion to HID usage 41 and the in-simulator runner to XCUIKeyboardKey.escape, and the Key union accepts it so it can be written at all.
This commit is contained in:
1 parent
4b7a3878ac
commit
da3cc5f68c
12 files changed
+106
-10
No files matched your search
@@ -46,12 +46,14 @@ enum TextInput {
|
|||||||
try typeOnFocus(deletes, bundleIdentifier: bundleIdentifier)
|
try typeOnFocus(deletes, bundleIdentifier: bundleIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
// pressKey types a single logical key into the focused field. Only return is
|
// pressKey types a single logical key into the focused field, matching the
|
||||||
// supported, matching the simulator companion's key surface.
|
// simulator companion's key surface.
|
||||||
static func pressKey(key: String, bundleIdentifier: String) throws {
|
static func pressKey(key: String, bundleIdentifier: String) throws {
|
||||||
switch key {
|
switch key {
|
||||||
case "return", "enter", "Return", "Enter":
|
case "return", "enter", "Return", "Enter":
|
||||||
try typeOnFocus(XCUIKeyboardKey.return.rawValue, bundleIdentifier: bundleIdentifier)
|
try typeOnFocus(XCUIKeyboardKey.return.rawValue, bundleIdentifier: bundleIdentifier)
|
||||||
|
case "escape", "Escape":
|
||||||
|
try typeOnFocus(XCUIKeyboardKey.escape.rawValue, bundleIdentifier: bundleIdentifier)
|
||||||
default:
|
default:
|
||||||
throw TextInputError.typingFailed("unsupported key \(key)")
|
throw TextInputError.typingFailed("unsupported key \(key)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -664,12 +664,15 @@ func (d *Driver) runnerTyper() transport.TextTyper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pressKeyUsage maps the logical key names mobile runs emit to a HID usage.
|
// pressKeyUsage maps the logical key names mobile runs emit to a HID usage.
|
||||||
// Only Return/Enter has a hardware-keyboard equivalent on the simulator; other
|
// Return/Enter and Escape are the hardware-keyboard keys the simulator has;
|
||||||
// names (notably "back" and "home") have no HID key and report unsupported.
|
// other names (notably "back" and "home") have no HID key and report
|
||||||
|
// unsupported.
|
||||||
func pressKeyUsage(key string) (uint32, bool) {
|
func pressKeyUsage(key string) (uint32, bool) {
|
||||||
switch key {
|
switch key {
|
||||||
case "enter", "return", "Enter", "Return":
|
case "enter", "return", "Enter", "Return":
|
||||||
return usageReturn, true
|
return usageReturn, true
|
||||||
|
case "escape", "Escape":
|
||||||
|
return usageEscape, true
|
||||||
default:
|
default:
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const (
|
|||||||
usage1 = 30
|
usage1 = 30
|
||||||
usage0 = 39
|
usage0 = 39
|
||||||
usageReturn = 40
|
usageReturn = 40
|
||||||
|
usageEscape = 41
|
||||||
usageTab = 43
|
usageTab = 43
|
||||||
usageSpace = 44
|
usageSpace = 44
|
||||||
usageBackspace = 42
|
usageBackspace = 42
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package ioscompanion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/priyanshujain/sanderling/internal/driver/ioscompanion/transport"
|
||||||
|
)
|
||||||
|
|
||||||
|
// keyRecordingCompanion keeps the HID events a press produced, so the assertion
|
||||||
|
// is over what reached the transport rather than over the lookup that built it.
|
||||||
|
type keyRecordingCompanion struct {
|
||||||
|
fakeCompanion
|
||||||
|
events []transport.HIDEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *keyRecordingCompanion) SendHID(
|
||||||
|
_ context.Context,
|
||||||
|
events ...transport.HIDEvent,
|
||||||
|
) error {
|
||||||
|
c.events = append(c.events, events...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// docs/manual/spec-language.md documents escape, and the simulator's HID stream
|
||||||
|
// carries it: usage 41 is the keyboard escape. Without it a spec clause over
|
||||||
|
// escape reports unsupported on iOS while the same clause runs on web.
|
||||||
|
func TestPressKeyEscapeReachesTheHIDStream(t *testing.T) {
|
||||||
|
companion := &keyRecordingCompanion{}
|
||||||
|
d := newTestDriver(companion)
|
||||||
|
|
||||||
|
if err := d.PressKey(context.Background(), "escape"); err != nil {
|
||||||
|
t.Fatalf("PressKey escape: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 41 is the USB HID keyboard escape usage, stated here rather than read
|
||||||
|
// from the production table so a wrong table entry cannot agree with itself.
|
||||||
|
want := []transport.HIDEvent{transport.KeyDown(41), transport.KeyUp(41)}
|
||||||
|
if len(companion.events) != len(want) {
|
||||||
|
t.Fatalf("sent %v, want %v", companion.events, want)
|
||||||
|
}
|
||||||
|
for index, event := range companion.events {
|
||||||
|
if event != want[index] {
|
||||||
|
t.Fatalf("sent %v, want %v", companion.events, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -336,7 +336,13 @@ func (c *runnerCompanion) PressKey(ctx context.Context, key string) error {
|
|||||||
case "enter", "return", "Enter", "Return":
|
case "enter", "return", "Enter", "Return":
|
||||||
_, err := c.call(ctx, "pressKey", map[string]any{"key": "return"})
|
_, err := c.call(ctx, "pressKey", map[string]any{"key": "return"})
|
||||||
return err
|
return err
|
||||||
|
case "escape", "Escape":
|
||||||
|
_, err := c.call(ctx, "pressKey", map[string]any{"key": "escape"})
|
||||||
|
return err
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("runner companion cannot press key %q; only return is supported", key)
|
return fmt.Errorf(
|
||||||
|
"runner companion cannot press key %q; only return and escape are supported",
|
||||||
|
key,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ type TextEditor interface {
|
|||||||
// EraseText deletes characterCount characters from the focused field.
|
// EraseText deletes characterCount characters from the focused field.
|
||||||
EraseText(ctx context.Context, characterCount int) error
|
EraseText(ctx context.Context, characterCount int) error
|
||||||
|
|
||||||
// PressKey presses the named logical key (currently only return/enter).
|
// PressKey presses the named logical key (return/enter and escape).
|
||||||
PressKey(ctx context.Context, key string) error
|
PressKey(ctx context.Context, key string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ export type Key =
|
|||||||
| "home"
|
| "home"
|
||||||
| "enter"
|
| "enter"
|
||||||
| "tab"
|
| "tab"
|
||||||
|
| "escape"
|
||||||
| "up"
|
| "up"
|
||||||
| "down"
|
| "down"
|
||||||
| "left"
|
| "left"
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import type {
|
|||||||
EventuallyFormula,
|
EventuallyFormula,
|
||||||
Extracted,
|
Extracted,
|
||||||
Formula,
|
Formula,
|
||||||
|
Key,
|
||||||
State,
|
State,
|
||||||
SanderlingRuntime,
|
SanderlingRuntime,
|
||||||
} from "../src/types.ts";
|
} from "../src/types.ts";
|
||||||
@@ -273,6 +274,26 @@ test("PressKey returns a PressKeyAction descriptor", () => {
|
|||||||
assert.deepEqual(PressKey({ key: "back" }), { kind: "PressKey", key: "back" });
|
assert.deepEqual(PressKey({ key: "back" }), { kind: "PressKey", key: "back" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Every key docs/manual/spec-language.md documents has to be authorable. A key
|
||||||
|
// the type rejects cannot appear in a spec at all, so the requirement it stands
|
||||||
|
// for ("escape discards the edit in progress") is never actuated anywhere.
|
||||||
|
test("PressKey accepts every documented key", () => {
|
||||||
|
const keys: Key[] = [
|
||||||
|
"back",
|
||||||
|
"home",
|
||||||
|
"enter",
|
||||||
|
"tab",
|
||||||
|
"escape",
|
||||||
|
"up",
|
||||||
|
"down",
|
||||||
|
"left",
|
||||||
|
"right",
|
||||||
|
];
|
||||||
|
for (const key of keys) {
|
||||||
|
assert.deepEqual(PressKey({ key }), { kind: "PressKey", key });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("Wait returns a WaitAction descriptor", () => {
|
test("Wait returns a WaitAction descriptor", () => {
|
||||||
assert.deepEqual(Wait({ durationMillis: 500 }), { kind: "Wait", durationMillis: 500 });
|
assert.deepEqual(Wait({ durationMillis: 500 }), { kind: "Wait", durationMillis: 500 });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -623,8 +623,8 @@ func (x *SwipeRequest) GetDurationMillis() int64 {
|
|||||||
|
|
||||||
type PressKeyRequest struct {
|
type PressKeyRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// Logical key name. Supported: "back", "home", "enter", "tab", "up",
|
// Logical key name. Supported: "back", "home", "enter", "tab", "escape",
|
||||||
// "down", "left", "right".
|
// "up", "down", "left", "right".
|
||||||
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ message SwipeRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message PressKeyRequest {
|
message PressKeyRequest {
|
||||||
// Logical key name. Supported: "back", "home", "enter", "tab", "up",
|
// Logical key name. Supported: "back", "home", "enter", "tab", "escape",
|
||||||
// "down", "left", "right".
|
// "up", "down", "left", "right".
|
||||||
string key = 1;
|
string key = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -513,6 +513,7 @@ class StubDriverBackend(
|
|||||||
"home" to "KEYCODE_HOME",
|
"home" to "KEYCODE_HOME",
|
||||||
"enter" to "KEYCODE_ENTER",
|
"enter" to "KEYCODE_ENTER",
|
||||||
"tab" to "KEYCODE_TAB",
|
"tab" to "KEYCODE_TAB",
|
||||||
|
"escape" to "KEYCODE_ESCAPE",
|
||||||
"up" to "KEYCODE_DPAD_UP",
|
"up" to "KEYCODE_DPAD_UP",
|
||||||
"down" to "KEYCODE_DPAD_DOWN",
|
"down" to "KEYCODE_DPAD_DOWN",
|
||||||
"left" to "KEYCODE_DPAD_LEFT",
|
"left" to "KEYCODE_DPAD_LEFT",
|
||||||
@@ -1207,6 +1208,7 @@ private fun keyCodeToMaestro(adbKeyCode: String): maestro.KeyCode? =
|
|||||||
"KEYCODE_HOME" -> maestro.KeyCode.HOME
|
"KEYCODE_HOME" -> maestro.KeyCode.HOME
|
||||||
"KEYCODE_ENTER" -> maestro.KeyCode.ENTER
|
"KEYCODE_ENTER" -> maestro.KeyCode.ENTER
|
||||||
"KEYCODE_TAB" -> maestro.KeyCode.TAB
|
"KEYCODE_TAB" -> maestro.KeyCode.TAB
|
||||||
|
"KEYCODE_ESCAPE" -> maestro.KeyCode.ESCAPE
|
||||||
"KEYCODE_DPAD_UP" -> maestro.KeyCode.REMOTE_UP
|
"KEYCODE_DPAD_UP" -> maestro.KeyCode.REMOTE_UP
|
||||||
"KEYCODE_DPAD_DOWN" -> maestro.KeyCode.REMOTE_DOWN
|
"KEYCODE_DPAD_DOWN" -> maestro.KeyCode.REMOTE_DOWN
|
||||||
"KEYCODE_DPAD_LEFT" -> maestro.KeyCode.REMOTE_LEFT
|
"KEYCODE_DPAD_LEFT" -> maestro.KeyCode.REMOTE_LEFT
|
||||||
|
|||||||
@@ -212,4 +212,17 @@ class InputTextTest {
|
|||||||
assertEquals(maestro.KeyCode.ENTER, maestroKeyFor("enter"))
|
assertEquals(maestro.KeyCode.ENTER, maestroKeyFor("enter"))
|
||||||
assertFailsWith<IllegalArgumentException> { maestroKeyFor("zorp") }
|
assertFailsWith<IllegalArgumentException> { maestroKeyFor("zorp") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// escape is a documented key. Missing from the table it throws on the adb
|
||||||
|
// backend and, when a table entry has no device-driver equivalent, the
|
||||||
|
// device backend used to drop the press with no error at all.
|
||||||
|
@Test fun escapeReachesBothAndroidBackends() {
|
||||||
|
val commands = mutableListOf<List<String>>()
|
||||||
|
StubDriverBackend("android") { commands.add(it) }.pressKey("escape")
|
||||||
|
assertEquals(
|
||||||
|
listOf(listOf("shell", "input", "keyevent", "KEYCODE_ESCAPE")),
|
||||||
|
commands,
|
||||||
|
)
|
||||||
|
assertEquals(maestro.KeyCode.ESCAPE, maestroKeyFor("escape"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user