Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ Upstream's third skill, `migrate-to-e2e` (`e6885dc`, `6609428`: move a suite one
- `VideoTests` (`describe('video')` of `engine.test.ts`) are ported in full on a scripted client; "records and stops on the pinned $platform device after closeApp ends the previous session" runs the second attempt as a new session. `ManifestTests` declares the video capability.
- Upstream's keyboard drag start (`fix(mobile): start the iOS keyboard-dismissing drag clear of controls`, `787a5d5`, tester-army/e2e#1305) is ported. `Keyboard.KeyboardDragStart` in `Keyboard.cs` is upstream's `keyboardDragStart` in `keyboard.ts` (the class name is the module's, since C# has no free functions). `DeviceSession.KeyboardTop` is upstream's `keyboardTop`, and `RawSnapshot.KeyboardFrame` carries the keyboard fact's frame it reads. `SnapshotProjection.ScreenKinds` is exported as upstream exports `SCREEN_KINDS`. `DismissKeyboardAsync` drags from that point, and a screen with none fails with `UNSUPPORTED_CAPABILITY` before any drag. Tests: five `it`s of `describe('device fixture')` in `Engine/DeviceFixtureDismissingAKeyboardThatHasNoDismissKeyTests`. Not ported: 'dismissing the keyboard over choice controls leaves them as they were' (`apps/mobile-benchmark/tests/device.e2e.ts`), which needs the mobile benchmark app, a React Native app the port lacks.
- Upstream's known-viewport fix (`fix(mobile): keep the known viewport when the app reads smaller than the screen`, `7674a2c`, tester-army/e2e#1291) is ported. `DeviceSession.Project` learns a snapshot's viewport only when it is plainly the screen's: the first one the session reads, the one it knows, or that one rotated (`SameScreen`, upstream's `sameScreen`; two sizes within a point are one screen, since rects from the tree carry fractions). `ViewportForAsync` (upstream's `viewportFor`) checks a tree of any other size against the device's screenshot, in either orientation, before it replaces the known one: a screenshot reports the portrait size even in landscape, so it confirms a size but cannot pick the orientation. The size it checks against is `DeviceSupport.LogicalScreenSize` (upstream's `logicalScreenSize`): the screenshot's logical dimensions, else its pixel dimensions over its density. A simulator's screenshot reports those; a real device's reports neither, so there the tree's size is taken as it comes. A screenshot that fails during that check fails the observation, as upstream's does. `TraceScreen` and `MaskedScreenshotAsync` size the trace screen and the secure-field masks by the known viewport, not the tree's. On iOS, a swipe from a sheet's grabber can pull down Notification Center, and the app's tree behind it reported a third of the screen (130x281 on a 390x844 device), which misplaced every rect and tap after it. Upstream records this only in its changeset; its skill references and docs say nothing about it, so `SKILL.md` and `docs/` have nothing to mirror. Tests: in `Engine/ObservationTests`, 'keeps the known viewport for an app drawn smaller than the screen, and follows a rotation' (new) and 'measures the screen in the viewport the snapshot publishes, and in its tree when it publishes none' (split into two harnesses, as upstream's); in `Engine/DeviceFixtureDismissingAKeyboardThatHasNoDismissKeyTests`, 'drags at the centre of a screen rotated since the last observation' (new), since the keyboard drag measures its start through `ViewportForAsync` too.
- Upstream's WebDriver element-type fix (`fix(mobile): read XCUIElementType class names as their XCTest types`, `3f9a7d8`, tester-army/e2e#1133) is ported. `SnapshotProjection.NormalizeKind` is upstream's `normalizeKind` in `nodes.ts`: it drops `^XCUIElementType(?=[A-Z])` (`XcuiElementTypePrefix`, a .NET `[GeneratedRegex]` for upstream's inline regex) before it maps the type, so the `XCUIElementTypeButton` that agent-device's WebDriver runtime reports on hosted Appium clouds reads as `Button`. `DeviceSession.KeyboardShowing` is upstream's `keyboardShowing` in `surface.ts`: it routes node types through `NormalizeKind`, so an `XCUIElementTypeKeyboard` counts as a shown keyboard. Upstream records this only in its changeset; its skill references and docs say nothing about it, so `SKILL.md` and `docs/` have nothing to mirror. Tests: in `Nodes/SnapshotProjectionTests`, 'reads the XCUIElementType class names a WebDriver session reports as the native XCTest types'; in `Engine/DeviceFixtureDismissingAKeyboardThatHasNoDismissKeyTests`, 'reads an XCUIElementTypeKeyboard from a WebDriver session as a shown keyboard'.
- Not ported: the tool-pixels test (needs the full runner and a scripted model), and the `device.openApp` check of an unknown permission name.

## EAS Simulators
Expand Down
2 changes: 1 addition & 1 deletion src/E2E.Mobile/DeviceSession.Screen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ internal async Task ActAsync(string label, Func<IDeviceClient, CancellationToken
/// counts as no keyboard: a drag on a guess costs more than a keyboard a later tap refuses against by name.
/// </summary>
private static bool KeyboardShowing(RawSnapshot raw) =>
MeasuredKeyboard(raw) ?? raw.Nodes.Any(node => KeyboardTypes.Contains((node.Type ?? "").ToLowerInvariant()));
MeasuredKeyboard(raw) ?? raw.Nodes.Any(node => KeyboardTypes.Contains(SnapshotProjection.NormalizeKind(node.Type ?? "")));

/// <summary>The top edge of the soft keyboard: the band the capture's producer measured, else the highest keyboard or key element, else the bottom of the screen.</summary>
private static double KeyboardTop(RawSnapshot raw, ViewportSize viewport) =>
Expand Down
7 changes: 5 additions & 2 deletions src/E2E.Mobile/SnapshotProjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,10 @@ public static partial class SnapshotProjection
/// <summary>The classes a status icon is built from: it shows, and offers nothing to act on.</summary>
private static readonly HashSet<string> StatusIconKinds = ["text-view", "image-view", "frame-layout", "linear-layout", "view", "view-group"];

/// <summary>One element-type spelling for <c>NavigationBar</c>, <c>navigation-bar</c>, and <c>android.widget.NavigationBar</c> alike.</summary>
/// <summary>One element-type spelling for <c>NavigationBar</c>, <c>XCUIElementTypeNavigationBar</c>, <c>navigation-bar</c>, and <c>android.widget.NavigationBar</c> alike.</summary>
internal static string NormalizeKind(string type)
{
var simple = type[(type.LastIndexOf('.') + 1)..];
var simple = XcuiElementTypePrefix().Replace(type[(type.LastIndexOf('.') + 1)..], "");
simple = WordBoundary().Replace(simple, "$1-$2");
return Separators().Replace(simple, "-").ToLowerInvariant();
}
Expand Down Expand Up @@ -733,6 +733,9 @@ private static bool IsSystemBar(RawNode node, ViewportSize? screen, Func<bool> d
[GeneratedRegex("([a-z0-9])([A-Z])")]
private static partial Regex WordBoundary();

[GeneratedRegex("^XCUIElementType(?=[A-Z])")]
private static partial Regex XcuiElementTypePrefix();

[GeneratedRegex(@"[\s_]+")]
private static partial Regex Separators();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,19 @@ public async Task Reads_the_keyboard_off_its_own_elements_when_the_capture_carri
Assert.Single(Swipes(h));
}

[Fact]
public async Task Reads_an_XCUIElementTypeKeyboard_from_a_WebDriver_session_as_a_shown_keyboard()
{
var (h, session) = await Ready();
Captures(
h,
() => FakeDeviceClient.SettingsSnapshotWith(
N("@e20", index: 10, parentIndex: 0, depth: 1, type: "XCUIElementTypeKeyboard", rect: new Rect(0, 583, 390, 261))),
FakeDeviceClient.SettingsSnapshot);
await session.Device.DismissKeyboardAsync();
Assert.Single(Swipes(h));
}

[Fact]
public async Task Retries_a_sparse_capture_instead_of_reading_it_as_a_dismissed_keyboard()
{
Expand Down
20 changes: 20 additions & 0 deletions tests/E2E.Mobile.Tests/Nodes/SnapshotProjectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,26 @@ public void Normalizes_XCTest_PascalCase_element_types_onto_the_same_vocabulary(
Assert.Equal("Settings", SnapshotProjection.ScreenTitle(projected));
}

[Fact]
public void Reads_the_XCUIElementType_class_names_a_WebDriver_session_reports_as_the_native_XCTest_types()
{
RawNode[] native =
[
N("e1", index: 0, depth: 0, type: "Application", label: "Profiler", rect: R(0, 0, 390, 844)),
N("e2", index: 1, parentIndex: 0, depth: 1, type: "NavigationBar", identifier: "Profiler"),
N("e3", index: 2, parentIndex: 1, depth: 2, type: "StaticText", label: "Profiler"),
N("e4", index: 3, parentIndex: 0, depth: 1, type: "Button", label: "CPU Load"),
N("e5", index: 4, parentIndex: 0, depth: 1, type: "TextView", label: "Notes", value: "idle"),
N("e6", index: 5, parentIndex: 0, depth: 1, type: "TabBar"),
N("e7", index: 6, parentIndex: 5, depth: 2, type: "Button", label: "Home", selected: true),
N("e8", index: 7, parentIndex: 5, depth: 2, type: "Button", label: "Settings"),
];
var webDriver = native.Select(node => node with { Type = $"XCUIElementType{node.Type}" }).ToArray();
static List<(string, string?)> KindsAndRoles(RawNode[] nodes) => [.. Project(nodes).Index.Select(entry => (entry.Kind, entry.Node.Role))];
Assert.Equal(KindsAndRoles(native), KindsAndRoles(webDriver));
Assert.Equal(new ViewportSize(390, 844), Project(webDriver).Viewport);
}

[Fact]
public void Maps_iOS_composite_widgets_onto_the_vocabulary_roles_a_browser_reports_for_them()
{
Expand Down
Loading