Skip to content

Commit 2014cb6

Browse files
authored
fix: harden covered snapshot targets (#708)
* fix: block covered snapshot targets * fix: harden covered snapshot targets
1 parent c89719f commit 2014cb6

14 files changed

Lines changed: 772 additions & 102 deletions

File tree

.github/workflows/ios.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ jobs:
4646
build-command: sh ./scripts/build-xcuitest-apple.sh
4747
xcuitest-platform: ios
4848
xcuitest-destination: generic/platform=iOS Simulator
49-
build-on-miss: 'false'
5049

5150
- name: Boot iOS test simulator
5251
uses: ./.github/actions/boot-ios-test-simulator

src/__tests__/runtime-interactions.test.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,38 @@ test('runtime click keeps distinct tab button centers when iOS reports the tab b
133133
assert.equal(selectorResult.node?.label, 'Settings');
134134
});
135135

136+
test('runtime click rejects refs covered by floating overlays', async () => {
137+
const calls: Point[] = [];
138+
const device = createInteractionDevice(coveredByTabBarSnapshot(), {
139+
tap: async (_context, point) => {
140+
calls.push(point);
141+
},
142+
});
143+
144+
await assert.rejects(
145+
() => device.interactions.click(ref('@e2'), { session: 'default' }),
146+
/Ref @e2 is covered by another visible element/,
147+
);
148+
assert.deepEqual(calls, []);
149+
});
150+
151+
test('runtime selector interactions skip covered matches when an uncovered duplicate exists', async () => {
152+
const calls: Point[] = [];
153+
const device = createInteractionDevice(duplicateCoveredLabelSnapshot(), {
154+
tap: async (_context, point) => {
155+
calls.push(point);
156+
},
157+
});
158+
159+
const result = await device.interactions.click(selector('label="Save draft"'), {
160+
session: 'default',
161+
});
162+
163+
assert.equal(result.kind, 'selector');
164+
assert.equal(result.node?.ref, 'e2');
165+
assert.deepEqual(calls, [{ x: 86, y: 142 }]);
166+
});
167+
136168
test('runtime click keeps non-button semantic targets at their own center', async () => {
137169
const calls: Point[] = [];
138170
const device = createInteractionDevice(nonHittableCellSnapshot(), {
@@ -875,6 +907,77 @@ function iosTabBarSnapshot(): SnapshotState {
875907
]);
876908
}
877909

910+
function coveredByTabBarSnapshot(): SnapshotState {
911+
return makeSnapshotState([
912+
{
913+
index: 0,
914+
depth: 0,
915+
type: 'Application',
916+
label: 'Example',
917+
rect: { x: 0, y: 0, width: 390, height: 844 },
918+
},
919+
{
920+
index: 1,
921+
depth: 1,
922+
parentIndex: 0,
923+
type: 'Button',
924+
label: 'Save draft',
925+
rect: { x: 16, y: 790, width: 140, height: 44 },
926+
hittable: false,
927+
interactionBlocked: 'covered',
928+
presentationHints: ['covered'],
929+
},
930+
{
931+
index: 2,
932+
depth: 1,
933+
parentIndex: 0,
934+
type: 'TabBar',
935+
rect: { x: 0, y: 760, width: 390, height: 84 },
936+
hittable: true,
937+
},
938+
]);
939+
}
940+
941+
function duplicateCoveredLabelSnapshot(): SnapshotState {
942+
return makeSnapshotState([
943+
{
944+
index: 0,
945+
depth: 0,
946+
type: 'Application',
947+
label: 'Example',
948+
rect: { x: 0, y: 0, width: 390, height: 844 },
949+
},
950+
{
951+
index: 1,
952+
depth: 1,
953+
parentIndex: 0,
954+
type: 'Button',
955+
label: 'Save draft',
956+
rect: { x: 16, y: 120, width: 140, height: 44 },
957+
hittable: true,
958+
},
959+
{
960+
index: 2,
961+
depth: 1,
962+
parentIndex: 0,
963+
type: 'Button',
964+
label: 'Save draft',
965+
rect: { x: 16, y: 790, width: 140, height: 44 },
966+
hittable: false,
967+
interactionBlocked: 'covered',
968+
presentationHints: ['covered'],
969+
},
970+
{
971+
index: 3,
972+
depth: 1,
973+
parentIndex: 0,
974+
type: 'TabBar',
975+
rect: { x: 0, y: 760, width: 390, height: 84 },
976+
hittable: true,
977+
},
978+
]);
979+
}
980+
878981
function nonHittableCellSnapshot(): SnapshotState {
879982
return makeSnapshotState([
880983
{

0 commit comments

Comments
 (0)