Skip to content

Commit d34bfeb

Browse files
committed
test(scroll): fix the climbing fixture and drop duplicated route-level cases
1 parent afa8b27 commit d34bfeb

2 files changed

Lines changed: 27 additions & 167 deletions

File tree

src/daemon/__tests__/scroll-runtime.test.ts

Lines changed: 10 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,12 @@ function untilNodes(targetY: number, hiddenBelow: boolean) {
361361
];
362362
}
363363

364-
test('bound scroll --until stops on the pass whose capture shows the selector on screen', async () => {
364+
/**
365+
* Route-level only: the executor's result envelope, the parse rejection, and admission. The loop's
366+
* own behavior — arrival, end-of-content, the pass budget, and every capture refusal — is covered
367+
* against the module in `scroll-until.test.ts` rather than duplicated through this harness.
368+
*/
369+
test('bound scroll --until reports the passes it spent and the selector it stopped on', async () => {
365370
const scrolls: string[] = [];
366371
const frames = [untilNodes(2400, true), untilNodes(1200, true), untilNodes(300, true)];
367372
const result = await runScroll(
@@ -377,118 +382,12 @@ test('bound scroll --until stops on the pass whose capture shows the selector on
377382
);
378383

379384
assert.equal(result.until, 'label=Email');
385+
assert.equal(result.direction, 'down');
380386
assert.equal(result.passes, 2);
381-
assert.equal(scrolls.length, 2);
387+
assert.deepEqual(scrolls, ['down', 'down']);
382388
assert.match(String(result.message), /Scrolled down 2 passes until label=Email was visible/);
383389
});
384390

385-
test('bound scroll --until reports the end of the content rather than spending its budget', async () => {
386-
await assert.rejects(
387-
() =>
388-
runScroll(
389-
['down'],
390-
{ until: 'label=Missing' },
391-
{
392-
captureSnapshot: async () => ({ nodes: untilNodes(300, false) }),
393-
scroll: async () => ({}),
394-
},
395-
),
396-
(error: unknown) => {
397-
assert.ok(error instanceof AppError);
398-
assert.equal(error.details?.reason, 'scroll_until_edge_reached');
399-
return true;
400-
},
401-
);
402-
});
403-
404-
test('bound scroll rejects --until on an edge direction before any device work', async () => {
405-
await assert.rejects(
406-
() =>
407-
runScroll(
408-
['bottom'],
409-
{ until: 'label=Email' },
410-
{
411-
captureSnapshot: async () => ({ nodes: untilNodes(300, true) }),
412-
scroll: async () => {
413-
throw new Error('scroll should be rejected before the backend call');
414-
},
415-
},
416-
),
417-
/scroll bottom already scrolls to the bottom edge and cannot take --until/,
418-
);
419-
});
420-
421-
test('bound scroll --until is refused when the owner advertises no capture', async () => {
422-
const resolved = await resolveBoundScrollRuntime({
423-
device: IOS_SIMULATOR,
424-
positionals: ['down'],
425-
context: { until: 'label=Email' } as DaemonCommandContext,
426-
...bindings({ scroll: async () => ({}) }),
427-
});
428-
assert.equal(resolved.ok, false);
429-
});
430-
431-
/** Same defect as the command runtime's: a failed read is not evidence that the content ran out. */
432-
/**
433-
* The owner's capture result spells the verdict `quality`, which is the shape this route actually
434-
* receives — an earlier version of this test asserted through `snapshotQuality` and passed while
435-
* the real field went unread. The scroll spy proves each refusal lands before any gesture, and the
436-
* sparse tree deliberately has content below the fold, so an edge verdict would be wrong there too.
437-
*/
438-
test('bound scroll --until reports an unreadable capture as a capture failure, not end-of-content', async () => {
439-
let scrolls = 0;
440-
await assert.rejects(
441-
() =>
442-
runScroll(
443-
['down'],
444-
{ until: 'label=Email' },
445-
{
446-
captureSnapshot: async () => ({}),
447-
scroll: async () => {
448-
scrolls += 1;
449-
return {};
450-
},
451-
},
452-
),
453-
(error: unknown) => {
454-
assert.ok(error instanceof AppError);
455-
assert.equal(error.details?.reason, 'scroll_until_capture_unreadable');
456-
assert.equal(error.details?.captureRefusal, 'no-capture');
457-
return true;
458-
},
459-
);
460-
assert.equal(scrolls, 0);
461-
});
462-
463-
test('bound scroll --until refuses a sparse capture before matching, edge analysis or scrolling', async () => {
464-
let scrolls = 0;
465-
await assert.rejects(
466-
() =>
467-
runScroll(
468-
['down'],
469-
{ until: 'label=Email' },
470-
{
471-
captureSnapshot: async () => ({
472-
nodes: untilNodes(2400, true),
473-
quality: { state: 'sparse', backend: 'tree', reason: 'AX bridge unavailable' },
474-
}),
475-
scroll: async () => {
476-
scrolls += 1;
477-
return {};
478-
},
479-
},
480-
),
481-
(error: unknown) => {
482-
assert.ok(error instanceof AppError);
483-
assert.equal(error.details?.reason, 'scroll_until_capture_unreadable');
484-
assert.equal(error.details?.captureRefusal, 'sparse-tree');
485-
assert.match(String(error.message), /AX bridge unavailable/);
486-
return true;
487-
},
488-
);
489-
assert.equal(scrolls, 0);
490-
});
491-
492391
test('bound scroll rejects --until on an edge direction before any device work', async () => {
493392
await assert.rejects(
494393
() =>
@@ -502,11 +401,11 @@ test('bound scroll rejects --until on an edge direction before any device work',
502401
},
503402
},
504403
),
505-
/scroll bottom already scrolls to the bottom edge and cannot take --until/,
404+
/cannot take --until/,
506405
);
507406
});
508407

509-
test('bound scroll --until is refused when the owner advertises no capture', async () => {
408+
test('bound scroll --until is refused at admission when the owner declares no capture', async () => {
510409
const resolved = await resolveBoundScrollRuntime({
511410
device: IOS_SIMULATOR,
512411
positionals: ['down'],
@@ -515,46 +414,3 @@ test('bound scroll --until is refused when the owner advertises no capture', asy
515414
});
516415
assert.equal(resolved.ok, false);
517416
});
518-
519-
/** Same defect as the command runtime's: a failed read is not evidence that the content ran out. */
520-
test('bound scroll --until reports an unreadable capture as a capture failure, not end-of-content', async () => {
521-
await assert.rejects(
522-
() =>
523-
runScroll(
524-
['down'],
525-
{ until: 'label=Email' },
526-
{
527-
captureSnapshot: async () => ({}),
528-
scroll: async () => ({}),
529-
},
530-
),
531-
(error: unknown) => {
532-
assert.ok(error instanceof AppError);
533-
assert.equal(error.details?.reason, 'scroll_until_capture_unreadable');
534-
assert.equal(error.details?.captureRefusal, 'no-capture');
535-
return true;
536-
},
537-
);
538-
});
539-
540-
test('bound scroll --until refuses a sparse capture rather than trusting its selectors', async () => {
541-
await assert.rejects(
542-
() =>
543-
runScroll(
544-
['down'],
545-
{ until: 'label=Email' },
546-
{
547-
captureSnapshot: async () => ({
548-
nodes: untilNodes(2400, true),
549-
snapshotQuality: { state: 'sparse', backend: 'tree', reason: 'AX bridge unavailable' },
550-
}),
551-
scroll: async () => ({}),
552-
},
553-
),
554-
(error: unknown) => {
555-
assert.ok(error instanceof AppError);
556-
assert.equal(error.details?.captureRefusal, 'sparse-tree');
557-
return true;
558-
},
559-
);
560-
});

test/integration/provider-scenarios/scroll-until.test.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { withProviderScenarioResource } from './harness.ts';
1313
* rather than on injected gestures because the Android gesture path runs through the persistent
1414
* helper, not an adb shell command the world can count.
1515
*/
16+
/** Two screens below the fold, climbing one screen per capture: visible on the third capture. */
17+
const ARRIVAL_PASSES = 2;
18+
1619
function climbingRow(): () => number {
1720
let captures = 0;
1821
return () => {
@@ -23,16 +26,20 @@ function climbingRow(): () => number {
2326
}
2427

2528
function climbingHierarchy(targetTop: () => number): () => string {
26-
return () =>
27-
[
29+
return () => {
30+
// Read the stateful position ONCE: calling it per bound advanced the row twice per capture and
31+
// produced an inverted rectangle on the first one.
32+
const top = targetTop();
33+
return [
2834
'<?xml version="1.0" encoding="UTF-8"?>',
2935
'<hierarchy rotation="0">',
3036
' <node index="0" text="" resource-id="com.android.settings:id/main_content_scrollable_container" class="android.widget.ScrollView" package="com.android.settings" content-desc="" bounds="[0,0][390,600]" clickable="false" enabled="true">',
3137
' <node index="0" text="Apps" resource-id="android:id/title" class="android.widget.TextView" package="com.android.settings" content-desc="" bounds="[24,124][152,178]" clickable="true" enabled="true" focusable="true" focused="false" />',
32-
` <node index="1" text="Terms" resource-id="com.android.settings:id/terms" class="android.widget.TextView" package="com.android.settings" content-desc="" bounds="[24,${targetTop()}][374,${targetTop() + 54}]" clickable="true" enabled="true" focusable="true" focused="false" />`,
38+
` <node index="1" text="Terms" resource-id="com.android.settings:id/terms" class="android.widget.TextView" package="com.android.settings" content-desc="" bounds="[24,${top}][374,${top + 54}]" clickable="true" enabled="true" focusable="true" focused="false" />`,
3339
' </node>',
3440
'</hierarchy>',
3541
].join('\n');
42+
};
3643
}
3744

3845
test('Provider-backed integration scroll --until stops on the capture that brings the target on screen', async () => {
@@ -48,18 +55,15 @@ test('Provider-backed integration scroll --until stops on the capture that bring
4855
...world.selection,
4956
});
5057

51-
const passes = typeof result.passes === 'number' ? result.passes : -1;
5258
assert.equal(result.until, 'text=Terms');
5359
assert.equal(result.direction, 'down');
54-
assert.ok(
55-
passes >= 1,
56-
`expected at least one pass to reach the off-screen row, saw ${passes}`,
57-
);
58-
assert.match(String(result.message), /until text=Terms was visible/);
59-
// Stopped on arrival rather than running the budget out.
60-
assert.ok(
61-
passes < SCROLL_UNTIL_PASS_LIMIT,
62-
`expected the loop to stop on arrival, spent ${passes} passes`,
60+
// An exact count is what proves repeated scrolling on valid geometry, rather than a lucky
61+
// first capture or a budget burned to the limit.
62+
assert.equal(result.passes, ARRIVAL_PASSES);
63+
assert.ok(ARRIVAL_PASSES < SCROLL_UNTIL_PASS_LIMIT);
64+
assert.match(
65+
String(result.message),
66+
new RegExp(`Scrolled down ${ARRIVAL_PASSES} passes until text=Terms was visible`),
6367
);
6468
},
6569
);

0 commit comments

Comments
 (0)