@keepsnappy/appium-driver 0.1.0-alpha.22 → 0.1.0-alpha.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/src/gestures.d.ts +41 -0
- package/dist/src/gestures.js +46 -6
- package/dist/src/gestures.js.map +1 -1
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.js +4 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/lists.d.ts +125 -18
- package/dist/src/lists.js +274 -104
- package/dist/src/lists.js.map +1 -1
- package/dist/src/run.js +2 -2
- package/dist/src/run.js.map +1 -1
- package/dist/src/scroll.d.ts +37 -15
- package/dist/src/scroll.js +196 -21
- package/dist/src/scroll.js.map +1 -1
- package/dist/src/session.d.ts +53 -9
- package/dist/src/session.js +101 -14
- package/dist/src/session.js.map +1 -1
- package/dist/src/webdriver.d.ts +22 -2
- package/dist/src/webdriver.js +79 -3
- package/dist/src/webdriver.js.map +1 -1
- package/package.json +1 -1
package/dist/src/lists.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describeSelector, toSelector } from './selectors.js';
|
|
2
|
-
import { LONG_SCROLL_CAP, LONG_SCROLL_PLANNED, IOS_TOP_EXTRA } from './gestures.js';
|
|
3
|
-
import { COAST_MAX_MS, GestureNotTaken, scrollList, waitCoasted } from './scroll.js';
|
|
2
|
+
import { LONG_SCROLL_CAP, LONG_SCROLL_PLANNED, TOP_STEP, IOS_TOP_EXTRA } from './gestures.js';
|
|
3
|
+
import { COAST_MAX_MS, COAST_POLL_MS, GestureNotTaken, NavigationStuck, rowEdges, rowsUnderList, scrollList, waitCoasted } from './scroll.js';
|
|
4
4
|
/** Default fixed end-pass gesture count. */
|
|
5
5
|
export const END_SCROLLS = 3;
|
|
6
6
|
/** Default end/top cycle count. */
|
|
@@ -20,6 +20,10 @@ export const LOOK_POLL_MS = 100;
|
|
|
20
20
|
export const CHECK_TIMEOUT_MS = 20_000;
|
|
21
21
|
/** Initial visibility wait before trying an indicator or fallback scroll. */
|
|
22
22
|
export const TARGET_SETTLE_MS = 10_000;
|
|
23
|
+
/** A navigation pass that still has not arrived after this long fails (it has no stroke count to end it). */
|
|
24
|
+
export const NAV_MAX_MS = 120_000;
|
|
25
|
+
/** Two navigation strokes in a row that leave the first and last visible rows as they were stop the pass. */
|
|
26
|
+
export const NAV_STUCK_STROKES = 2;
|
|
23
27
|
/** Finds any part of target on screen through a list handle. Logs lookup/frame commands only. */
|
|
24
28
|
async function shownIn(t, listEl, target) {
|
|
25
29
|
const { childOf: _list, ...inList } = toSelector(target);
|
|
@@ -36,13 +40,14 @@ async function shownIn(t, listEl, target) {
|
|
|
36
40
|
* begins at least lookWindowMs (1000) after it; lookPollMs defaults to 100. Optional settle runs before those looks.
|
|
37
41
|
* Logs cmd with the gesture count under field, blind/cap, looks/lookMs, status, duration, optional extra and planned overrun.
|
|
38
42
|
*/
|
|
39
|
-
export async function scrollUntilShown(t, { cmd, field, cap, blind = 0, planned, settle, look: once, move, missing, extra = () => ({}), lookWindowMs = LOOK_MS, lookPollMs = LOOK_POLL_MS }) {
|
|
43
|
+
export async function scrollUntilShown(t, { cmd, field, cap, blind = 0, planned, settle, look: once, move, missing, extra = () => ({}), lookWindowMs = LOOK_MS, lookPollMs = LOOK_POLL_MS, soft = false, lookBetweenBlind = false }) {
|
|
40
44
|
await t.ready();
|
|
41
45
|
const start = t.now();
|
|
42
46
|
let moves = 0;
|
|
43
47
|
let looks = 0;
|
|
44
48
|
let lookMs = 0;
|
|
45
49
|
let status = 1;
|
|
50
|
+
let reached = true;
|
|
46
51
|
try {
|
|
47
52
|
const look = async () => {
|
|
48
53
|
if (settle && moves > 0)
|
|
@@ -65,12 +70,36 @@ export async function scrollUntilShown(t, { cmd, field, cap, blind = 0, planned,
|
|
|
65
70
|
lookMs += t.now() - at;
|
|
66
71
|
}
|
|
67
72
|
};
|
|
73
|
+
let shownEarly = false;
|
|
68
74
|
for (; moves < blind; moves++) {
|
|
75
|
+
// A touch on a list that still coasts stops it (iOS): each blind move waits for the one before to come to rest.
|
|
76
|
+
if (moves > 0 && settle)
|
|
77
|
+
await settle();
|
|
78
|
+
if (moves > 0 && lookBetweenBlind) {
|
|
79
|
+
looks++;
|
|
80
|
+
if (await once()) {
|
|
81
|
+
shownEarly = true;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
69
85
|
await move();
|
|
70
86
|
}
|
|
87
|
+
if (shownEarly) {
|
|
88
|
+
status = 0;
|
|
89
|
+
return moves;
|
|
90
|
+
}
|
|
91
|
+
// All of a soft budget sent blind: nothing is left to decide in the step (the navigation pass after it looks).
|
|
92
|
+
if (soft && blind >= cap && moves >= cap) {
|
|
93
|
+
status = 0;
|
|
94
|
+
reached = false;
|
|
95
|
+
return moves;
|
|
96
|
+
}
|
|
71
97
|
while (!(await look())) {
|
|
72
98
|
if (moves >= cap) {
|
|
73
|
-
|
|
99
|
+
if (!soft)
|
|
100
|
+
throw new Error(missing(cap));
|
|
101
|
+
reached = false;
|
|
102
|
+
break;
|
|
74
103
|
}
|
|
75
104
|
await move();
|
|
76
105
|
moves++;
|
|
@@ -80,22 +109,29 @@ export async function scrollUntilShown(t, { cmd, field, cap, blind = 0, planned,
|
|
|
80
109
|
}
|
|
81
110
|
finally {
|
|
82
111
|
const over = planned === undefined ? {} : { extra: Math.max(0, moves - planned) };
|
|
83
|
-
t.driver.log.push({ ...t.driver.context, cmd, t: t.now(), ms: t.now() - start, status, [field]: moves, blind, cap, looks, lookMs, ...over, ...extra() });
|
|
112
|
+
t.driver.log.push({ ...t.driver.context, cmd, t: t.now(), ms: t.now() - start, status, [field]: moves, blind, cap, looks, lookMs, ...over, ...(reached ? {} : { reached }), ...extra() });
|
|
84
113
|
}
|
|
85
114
|
}
|
|
86
115
|
/** Platform gesture budget: cap (3), plus iosTopExtra (2) only when returning to the top on iOS. No commands or logs. */
|
|
87
116
|
export const scrollCap = (t, toward, cap = LONG_SCROLL_CAP, iosTopExtra = IOS_TOP_EXTRA) => cap + (t.driver.platform.name === 'ios' && toward === 'top' ? iosTopExtra : 0);
|
|
88
117
|
const iosHold = (t, cache) => t.driver.platform.name === 'ios' ? (cache.hold ??= {}) : undefined;
|
|
89
118
|
/**
|
|
90
|
-
*
|
|
119
|
+
* Brings `target` of `list` into view toward `toward`. Outside a measured step it is a navigation pass (`navigate`: marked `nav`,
|
|
120
|
+
* the navigation stroke, no gesture cap: it ends on the target found by lookup and the rows at rest, and stops when two strokes
|
|
121
|
+
* leave the rows as they were; `blind`, `cap`, `planned`, `iosTopExtra` and `used` do not apply). Inside a step its gestures stay
|
|
122
|
+
* the measured gesture, as below, and nothing is marked `nav`.
|
|
123
|
+
*
|
|
124
|
+
* In a step it long-scrolls list toward a partly visible target; scoped defaults true. Refetches missing/stale list handles before
|
|
91
125
|
* moving and rereads the frame before every gesture. A shared cache stores the settled frame, held iOS row and moves used.
|
|
92
126
|
* Defaults: 3 gestures (5 to top on iOS), 2 planned, 20 s frameTimeoutMs, 3 s coast. used subtracts the prior pass budget.
|
|
93
|
-
* A to-top gesture is checked (scrollList `mustMove`); its retry is a logged defect and does not use the budget.
|
|
127
|
+
* A to-top gesture is checked (scrollList `mustMove`); its retry is a logged defect and does not use the budget. With `soft`, the
|
|
128
|
+
* cap ends the loop quietly (`reached: false` in the log) where it would throw.
|
|
94
129
|
* Logs fling-to-end/top with flings, blind/cap, looks/lookMs, extra, relists, settled, status and duration, including failures.
|
|
95
130
|
*/
|
|
96
|
-
export async function flingUntilShown(t, list, target, toward, { scoped = true, blind = 0, cache = {}, row, used = 0, cap = LONG_SCROLL_CAP, planned = LONG_SCROLL_PLANNED, iosTopExtra = IOS_TOP_EXTRA, frameTimeoutMs = CHECK_TIMEOUT_MS, coastMaxMs = COAST_MAX_MS, lookWindowMs, lookPollMs } = {}) {
|
|
131
|
+
export async function flingUntilShown(t, list, target, toward, { scoped = true, blind = 0, cache = {}, row, used = 0, cap = LONG_SCROLL_CAP, planned = LONG_SCROLL_PLANNED, iosTopExtra = IOS_TOP_EXTRA, frameTimeoutMs = CHECK_TIMEOUT_MS, coastMaxMs = COAST_MAX_MS, lookWindowMs, lookPollMs, soft } = {}) {
|
|
97
132
|
let relists = 0;
|
|
98
133
|
let settled = false;
|
|
134
|
+
let sent = 0;
|
|
99
135
|
const { childOf: _list, ...anywhere } = toSelector(target);
|
|
100
136
|
const frame = async () => {
|
|
101
137
|
if (!cache.at) {
|
|
@@ -104,45 +140,59 @@ export async function flingUntilShown(t, list, target, toward, { scoped = true,
|
|
|
104
140
|
}
|
|
105
141
|
return cache.at;
|
|
106
142
|
};
|
|
143
|
+
const look = async () => {
|
|
144
|
+
const at = await frame();
|
|
145
|
+
if (!scoped) {
|
|
146
|
+
// Under the list handle when the rows are there; the target need not be a row (a header): a miss there asks the whole screen.
|
|
147
|
+
if ((await rowsUnderList(t, row ?? target, cache)) && (await t.driver.findShown(toSelector(anywhere), false, at.el)))
|
|
148
|
+
return true;
|
|
149
|
+
return Boolean(await t.driver.findShown(toSelector(anywhere)));
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
if (await shownIn(t, at.el, target)) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
if (e?.wdError !== 'stale element reference') {
|
|
158
|
+
throw e;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
cache.at = { ...at, el: await t.waitVisible(list) };
|
|
162
|
+
relists++;
|
|
163
|
+
try {
|
|
164
|
+
return await shownIn(t, cache.at.el, target);
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
if (e?.wdError !== 'stale element reference') {
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
if (t.stepDepth === 0 && !t.navigating) {
|
|
174
|
+
// A navigation stroke coasts far longer than the measured gesture: the pass keeps navigate's own rest bound.
|
|
175
|
+
await navigate(t, list, toward, { until: look, row: row ?? target, scoped, cache, frameTimeoutMs, label: selectorLabel(target), turnRound: true });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
107
178
|
cache.used = await scrollUntilShown(t, {
|
|
108
179
|
cmd: `fling-to-${toward}`,
|
|
109
|
-
field: 'flings', lookWindowMs, lookPollMs,
|
|
180
|
+
field: 'flings', lookWindowMs, lookPollMs, soft,
|
|
181
|
+
// A to-top gesture with the first row shown can drag a sheet closed (F7): blind to-top gestures stop once it shows.
|
|
182
|
+
lookBetweenBlind: toward === 'top',
|
|
110
183
|
cap: Math.max(0, scrollCap(t, toward, cap, iosTopExtra) - used),
|
|
111
184
|
planned: Math.max(0, planned - used),
|
|
112
185
|
blind: Math.min(blind, Math.max(0, scrollCap(t, toward, cap, iosTopExtra) - used)),
|
|
113
|
-
settle: () => waitCoasted(t, row ?? target, scoped ? { sel: list, el: cache.at?.el } : undefined, undefined, coastMaxMs, iosHold(t, cache)),
|
|
186
|
+
settle: async () => waitCoasted(t, row ?? target, scoped || (await rowsUnderList(t, row ?? target, cache)) ? { sel: list, el: cache.at?.el } : undefined, undefined, coastMaxMs, iosHold(t, cache)),
|
|
114
187
|
extra: () => ({ relists, settled }),
|
|
115
|
-
|
|
116
|
-
const at = await frame();
|
|
117
|
-
if (!scoped) {
|
|
118
|
-
return Boolean(await t.driver.findShown(toSelector(anywhere)));
|
|
119
|
-
}
|
|
120
|
-
try {
|
|
121
|
-
if (await shownIn(t, at.el, target)) {
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
catch (e) {
|
|
126
|
-
if (e?.wdError !== 'stale element reference') {
|
|
127
|
-
throw e;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
cache.at = { ...at, el: await t.waitVisible(list) };
|
|
131
|
-
relists++;
|
|
132
|
-
try {
|
|
133
|
-
return await shownIn(t, cache.at.el, target);
|
|
134
|
-
}
|
|
135
|
-
catch (e) {
|
|
136
|
-
if (e?.wdError !== 'stale element reference') {
|
|
137
|
-
throw e;
|
|
138
|
-
}
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
},
|
|
188
|
+
look,
|
|
142
189
|
async move() {
|
|
143
190
|
await frame();
|
|
144
|
-
// Before a to-top gesture the first row was not shown, so the list has room: the gesture is checked.
|
|
145
|
-
|
|
191
|
+
// Before a looked-for to-top gesture the first row was not shown, so the list has room: the gesture is checked. Of blind
|
|
192
|
+
// gestures only the first is: the list may reach its top before the last.
|
|
193
|
+
const checked = toward === 'top' && (sent === 0 || blind === 0);
|
|
194
|
+
sent++;
|
|
195
|
+
await scrollList(t, { list, row: row ?? target, scoped }, toward, toward === 'top' ? { cache, mustMove: checked, hold: iosHold(t, cache), coastMaxMs } : { cache });
|
|
146
196
|
},
|
|
147
197
|
missing: () => `${selectorLabel(target)} not on screen after ${scrollCap(t, toward, cap, iosTopExtra)} long scrolls to the ${toward} of ${selectorLabel(list)}`,
|
|
148
198
|
});
|
|
@@ -151,6 +201,99 @@ const selectorLabel = (sel) => {
|
|
|
151
201
|
const s = toSelector(sel);
|
|
152
202
|
return s.text ?? s.id ?? describeSelector(s);
|
|
153
203
|
};
|
|
204
|
+
/**
|
|
205
|
+
* A navigation pass: gets `list` toward its top or end until `until` finds the target, with the navigation stroke, between `nav`
|
|
206
|
+
* markers (`Session.navigation`; never inside a measured step). Before the pass and after each stroke it waits for the list to
|
|
207
|
+
* come to rest: its first and last row (`rowEdges`) read twice, 300 ms apart, are the same. The pass ends when the target is
|
|
208
|
+
* found by lookup with the list at rest; toward the end, a stroke that rests with no target waits `pageWaitMs` for one (a page
|
|
209
|
+
* lands only once the list rests at its loaded end) before the next. No frame test decides arrival. There is no stroke count: when
|
|
210
|
+
* two strokes in a row leave those rows as they were after the list had moved, the target was passed: the pass turns round once
|
|
211
|
+
* with `NAV_STROKE.precise` drags; when two strokes leave them as they were again (or the list never moved) it throws `NavigationStuck` (counted in `t.defects.navStuck`; the run
|
|
212
|
+
* is invalid), and `maxMs` bounds the pass in time. Logs `nav` with toward, strokes, arrived, status and ms, also on failure.
|
|
213
|
+
*/
|
|
214
|
+
export async function navigate(t, list, toward, { until, row, field, scoped = true, cache = {}, pageWaitMs = 0, coastMaxMs, frameTimeoutMs = CHECK_TIMEOUT_MS, maxMs = NAV_MAX_MS, label = selectorLabel(list), catchUp = false, turnRound = false, restBefore = true }) {
|
|
215
|
+
await t.ready();
|
|
216
|
+
const start = t.now();
|
|
217
|
+
const rest = coastMaxMs ?? (t.driver.platform.name === 'ios' ? PAGE_COAST_MAX_MS_IOS : PAGE_COAST_MAX_MS);
|
|
218
|
+
const spec = { list, row, field, scoped };
|
|
219
|
+
let strokes = 0;
|
|
220
|
+
let arrived = false;
|
|
221
|
+
try {
|
|
222
|
+
await t.navigation(async () => {
|
|
223
|
+
if (!cache.at)
|
|
224
|
+
cache.at = await t.settledFrame(list, frameTimeoutMs);
|
|
225
|
+
// A stroke coasts several screens: one can carry a mid-list target past the view to the list's edge. Stalled at that edge
|
|
226
|
+
// with the target unseen, the pass turns round once with drags that move a list less than its own height (no coast).
|
|
227
|
+
let way = toward;
|
|
228
|
+
let turned = false;
|
|
229
|
+
let moved = false;
|
|
230
|
+
const atRest = async () => {
|
|
231
|
+
const deadline = t.now() + rest;
|
|
232
|
+
let reads = 1;
|
|
233
|
+
for (let last = await rowEdges(t, spec, cache);;) {
|
|
234
|
+
await t.driver.sleep(COAST_POLL_MS);
|
|
235
|
+
const now = await rowEdges(t, spec, cache);
|
|
236
|
+
reads++;
|
|
237
|
+
// An empty read (no row found: a list re-rendering as a page lands) is never a rest.
|
|
238
|
+
if (now === last && now !== '')
|
|
239
|
+
return now;
|
|
240
|
+
// The bound is for a list that keeps moving, so it needs at least two comparisons: one read can outlast it on a real
|
|
241
|
+
// device (A23 gate, iPhone in TapIn's live room: a row lookup took 6 s, the first read found no row, the second came at 16 s).
|
|
242
|
+
if (t.now() >= deadline && reads >= 3)
|
|
243
|
+
throw new Error(`${selectorLabel(list)} still moving ${rest} ms ${strokes === 0 ? 'before the first navigation stroke' : 'after a navigation stroke'} toward the ${way}`);
|
|
244
|
+
last = now;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
const waitFor = async () => {
|
|
248
|
+
for (const from = t.now();;) {
|
|
249
|
+
if (await until())
|
|
250
|
+
return true;
|
|
251
|
+
if (t.now() - from >= pageWaitMs)
|
|
252
|
+
return false;
|
|
253
|
+
await t.driver.sleep(PAGE_POLL_MS);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
// A caller that saw the list at rest (an end step that waited out its coast and its page) takes one read for the stall
|
|
257
|
+
// baseline instead of two (gate 41000166764: a read is a 2.2 s page source in the live room).
|
|
258
|
+
let rows = restBefore ? await atRest() : await rowEdges(t, spec, cache);
|
|
259
|
+
let before = null;
|
|
260
|
+
let stalled = 0;
|
|
261
|
+
for (;;) {
|
|
262
|
+
if (strokes === 0 ? await until() : await waitFor()) {
|
|
263
|
+
arrived = true;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (before !== null)
|
|
267
|
+
stalled = rows === before ? stalled + 1 : 0;
|
|
268
|
+
if (before !== null && rows !== before)
|
|
269
|
+
moved = true;
|
|
270
|
+
// A list no stroke ever moved is stuck as it is; one that moved and then stopped stands at an edge.
|
|
271
|
+
if (stalled >= NAV_STUCK_STROKES && turnRound && !turned && moved) {
|
|
272
|
+
turned = true;
|
|
273
|
+
way = toward === 'end' ? 'top' : 'end';
|
|
274
|
+
stalled = 0;
|
|
275
|
+
before = null;
|
|
276
|
+
}
|
|
277
|
+
if (stalled >= NAV_STUCK_STROKES) {
|
|
278
|
+
t.defects.navStuck++;
|
|
279
|
+
throw new NavigationStuck(`${selectorLabel(list)} did not move after ${NAV_STUCK_STROKES} navigation strokes toward the ${way} (${label} not found; rows ${JSON.stringify(rows.split('\n'))})`);
|
|
280
|
+
}
|
|
281
|
+
if (t.now() - start > maxMs)
|
|
282
|
+
throw new Error(`${label} not reached after ${maxMs} ms of navigation toward the ${way} of ${selectorLabel(list)}`);
|
|
283
|
+
before = rows;
|
|
284
|
+
await scrollList(t, spec, way, { cache, ...(turned ? { precise: true } : {}) });
|
|
285
|
+
strokes++;
|
|
286
|
+
rows = await atRest();
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
if (catchUp && strokes > 0)
|
|
292
|
+
t.defects.navCatchUp++;
|
|
293
|
+
t.driver.log.push({ ...t.driver.context, cmd: 'nav', t: t.now(), ms: t.now() - start, status: arrived ? 0 : 1, toward, list: selectorLabel(list), strokes, arrived });
|
|
294
|
+
}
|
|
295
|
+
return { strokes, arrived, ms: t.now() - start };
|
|
296
|
+
}
|
|
154
297
|
/** Keeps parent scoping only when requested. */
|
|
155
298
|
function shownOf(row, scoped) {
|
|
156
299
|
const { childOf: _list, ...anywhere } = toSelector(row);
|
|
@@ -178,17 +321,17 @@ export function passChecked(t, list, target, toward, scoped, cache, row, options
|
|
|
178
321
|
};
|
|
179
322
|
}
|
|
180
323
|
/** Finds a loaded row without checking its frame; a stale parent is reported to the caller. Logs lookups only. */
|
|
181
|
-
async function rowLoaded(t, list, row, scoped, listEl, field) {
|
|
182
|
-
const found = await loadedRow(t, list, row, scoped, listEl, field);
|
|
324
|
+
async function rowLoaded(t, list, row, scoped, listEl, field, under) {
|
|
325
|
+
const found = await loadedRow(t, list, row, scoped, listEl, field, under);
|
|
183
326
|
return found === 'stale' ? found : found !== null;
|
|
184
327
|
}
|
|
185
|
-
async function loadedRow(t, list, row, scoped, listEl, field) {
|
|
328
|
+
async function loadedRow(t, list, row, scoped, listEl, field, under) {
|
|
186
329
|
const { childOf: _list, ...inList } = toSelector(row);
|
|
187
330
|
const all = t.driver.platform.locators(inList);
|
|
188
331
|
const call = field === 'text' ? /textMatches/ : /descriptionMatches/;
|
|
189
332
|
const locators = t.driver.platform.name === 'android' && field ? all.filter(l => call.test(l.value)) : all;
|
|
190
333
|
try {
|
|
191
|
-
const parent = scoped ? (listEl ?? (await t.waitVisible(list))) :
|
|
334
|
+
const parent = scoped ? (listEl ?? (await t.waitVisible(list))) : under;
|
|
192
335
|
for (const { using, value } of locators.length ? locators : all) {
|
|
193
336
|
const el = await t.driver.find(using, value, parent);
|
|
194
337
|
if (el)
|
|
@@ -215,26 +358,27 @@ async function rowEndsInside(t, list, listRect, row, scoped, listEl, field) {
|
|
|
215
358
|
return t.driver.platform.name === 'android' ? r.y + r.height < bottom : r.y + r.height <= bottom;
|
|
216
359
|
}
|
|
217
360
|
/**
|
|
218
|
-
* Performs scrolls (3) end gestures
|
|
219
|
-
* (
|
|
220
|
-
* for the page's first loaded row, polling every pagePollMs (300)
|
|
221
|
-
*
|
|
222
|
-
*
|
|
361
|
+
* Performs scrolls (3) end gestures, exactly that many and no other: a page that has not landed never gets a catch-up gesture
|
|
362
|
+
* here (a navigation pass outside the step fetches it). A gesture with no page marker pauses pauseMs (500). Otherwise it waits
|
|
363
|
+
* for coast (Android 6000 ms, iOS 10000), then waitMs (3000) for the page's first loaded row, polling every pagePollMs (300),
|
|
364
|
+
* counted from the coast. A page that has not landed by then fails the pass, or with `deferPages` is logged as not landed.
|
|
365
|
+
* scoped defaults true; field narrows Android lookups. `beforePages` is no longer read.
|
|
223
366
|
* The pass is taken to start at the top, so its first gesture is checked (scrollList `mustMove`): one that moved nothing is
|
|
224
367
|
* sent once more and counted in `t.defects`; `unmoved` is 1 then, and 2 when the retry moved nothing either (GestureNotTaken).
|
|
225
|
-
* Logs fling-to-end: flings, supplied pageNumbers (or descriptions), coastMs, waitMs, per
|
|
226
|
-
* status and duration. Page checks do not require visibility. All limits and frameTimeoutMs (20000) are configurable.
|
|
368
|
+
* Logs fling-to-end: flings, supplied pageNumbers (or descriptions), coastMs, waitMs, landed (per page), unmoved, pauseMs,
|
|
369
|
+
* settled, status and duration. Page checks do not require visibility. All limits and frameTimeoutMs (20000) are configurable.
|
|
227
370
|
*/
|
|
228
|
-
export async function scrollToEnd(t, list, cache, { pages, pageNumbers,
|
|
371
|
+
export async function scrollToEnd(t, list, cache, { pages, pageNumbers, scoped = true, field, any, scrolls = END_SCROLLS, pauseMs = PAGE_PAUSE_MS, pageWaitMs = PAGE_WAIT_MS, pagePollMs = PAGE_POLL_MS, coastMaxMs = PAGE_COAST_MAX_MS, coastMaxMsIos = PAGE_COAST_MAX_MS_IOS, frameTimeoutMs = CHECK_TIMEOUT_MS, deferPages = false }) {
|
|
229
372
|
await t.ready();
|
|
230
373
|
const start = t.now();
|
|
231
374
|
let status = 1;
|
|
232
375
|
let settled = false;
|
|
233
376
|
const coastMs = [];
|
|
234
377
|
const waitMs = [];
|
|
235
|
-
const
|
|
378
|
+
const landed = [];
|
|
236
379
|
let unmoved = 0;
|
|
237
380
|
const spec = { list, row: any, field, scoped };
|
|
381
|
+
const coastBound = t.driver.platform.name === 'ios' ? coastMaxMsIos : coastMaxMs;
|
|
238
382
|
try {
|
|
239
383
|
if (!cache.at) {
|
|
240
384
|
cache.at = await t.settledFrame(list, frameTimeoutMs);
|
|
@@ -244,7 +388,7 @@ export async function scrollToEnd(t, list, cache, { pages, pageNumbers, beforePa
|
|
|
244
388
|
// A pass starts at the top, so its first fling has room and is checked.
|
|
245
389
|
let sent;
|
|
246
390
|
try {
|
|
247
|
-
sent = await scrollList(t, spec, 'end', { cache, hold: iosHold(t, cache), mustMove: i === 0, coastMaxMs:
|
|
391
|
+
sent = await scrollList(t, spec, 'end', { cache, hold: iosHold(t, cache), mustMove: i === 0, coastMaxMs: coastBound });
|
|
248
392
|
}
|
|
249
393
|
catch (e) {
|
|
250
394
|
if (e instanceof GestureNotTaken)
|
|
@@ -259,66 +403,38 @@ export async function scrollToEnd(t, list, cache, { pages, pageNumbers, beforePa
|
|
|
259
403
|
continue;
|
|
260
404
|
}
|
|
261
405
|
const coastFrom = t.now();
|
|
262
|
-
const stopped = await waitCoasted(t, any, scoped ? { sel: list, el: cache.at.el } : undefined, field,
|
|
406
|
+
const stopped = await waitCoasted(t, any, scoped || (await rowsUnderList(t, any, cache)) ? { sel: list, el: cache.at.el } : undefined, field, coastBound, iosHold(t, cache));
|
|
263
407
|
coastMs.push(t.now() - coastFrom);
|
|
264
408
|
if (!stopped)
|
|
265
|
-
throw new Error(`${selectorLabel(list)} still moving ${
|
|
266
|
-
|
|
267
|
-
const before = beforePages[i];
|
|
268
|
-
let reached = true;
|
|
269
|
-
if (before && t.driver.platform.name === 'android') {
|
|
270
|
-
let found = await rowLoaded(t, list, before, scoped, cache.at.el, field);
|
|
271
|
-
if (found === 'stale' || (found !== true && scoped)) {
|
|
272
|
-
cache.at = { ...cache.at, el: await t.waitVisible(list) };
|
|
273
|
-
found = await rowLoaded(t, list, before, scoped, cache.at.el, field);
|
|
274
|
-
}
|
|
275
|
-
reached = found === true;
|
|
276
|
-
}
|
|
277
|
-
else if (before) {
|
|
278
|
-
reached = await t.isVisible(shownOf(before, scoped));
|
|
279
|
-
}
|
|
280
|
-
const again = async () => {
|
|
281
|
-
extra[i] = 1;
|
|
282
|
-
await scrollList(t, spec, 'end', { cache });
|
|
283
|
-
if (!(await waitCoasted(t, any, scoped ? { sel: list, el: cache.at.el } : undefined, field, (t.driver.platform.name === 'ios' ? coastMaxMsIos : coastMaxMs), iosHold(t, cache)))) {
|
|
284
|
-
throw new Error(`${selectorLabel(list)} still moving ${(t.driver.platform.name === 'ios' ? coastMaxMsIos : coastMaxMs)} ms after a long scroll to the end`);
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
if (!reached)
|
|
288
|
-
await again();
|
|
289
|
-
let from = t.now();
|
|
409
|
+
throw new Error(`${selectorLabel(list)} still moving ${coastBound} ms after a long scroll to the end`);
|
|
410
|
+
const from = t.now();
|
|
290
411
|
let listEl = cache.at.el;
|
|
291
412
|
let refound = !scoped;
|
|
413
|
+
let found = false;
|
|
292
414
|
for (;;) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (found === 'stale' || !refound) {
|
|
415
|
+
// Unscoped Android rows that sit under the list are looked up under its handle (a whole-screen lookup costs ~4×).
|
|
416
|
+
const under = !scoped && (await rowsUnderList(t, any, cache)) ? listEl : undefined;
|
|
417
|
+
found = await rowLoaded(t, list, page, scoped, listEl, field, under);
|
|
418
|
+
if (found === 'stale' || (found !== true && !refound)) {
|
|
297
419
|
refound = true;
|
|
298
420
|
listEl = await t.waitVisible(list);
|
|
299
421
|
cache.at = { ...cache.at, el: listEl };
|
|
300
|
-
|
|
301
|
-
break;
|
|
302
|
-
}
|
|
303
|
-
if (t.now() - from >= pageWaitMs) {
|
|
304
|
-
// The app loads a page only once the list rests at its loaded end; a cut last row means it rests short of it.
|
|
305
|
-
if (before && !extra[i] && !(await rowEndsInside(t, list, cache.at.rect, before, scoped, listEl, field))) {
|
|
306
|
-
await again();
|
|
307
|
-
from = t.now();
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
waitMs.push(t.now() - from);
|
|
311
|
-
throw new Error(`${selectorLabel(page)} not loaded ${pageWaitMs} ms after a long scroll to the end of ${selectorLabel(list)}`);
|
|
422
|
+
found = await rowLoaded(t, list, page, scoped, listEl, field, under === undefined ? undefined : listEl);
|
|
312
423
|
}
|
|
424
|
+
if (found === true || t.now() - from >= pageWaitMs)
|
|
425
|
+
break;
|
|
313
426
|
await t.driver.sleep(pagePollMs);
|
|
314
427
|
}
|
|
315
428
|
waitMs.push(t.now() - from);
|
|
429
|
+
landed.push(found === true);
|
|
430
|
+
if (found !== true && !deferPages)
|
|
431
|
+
throw new Error(`${selectorLabel(page)} not loaded ${pageWaitMs} ms after a long scroll to the end of ${selectorLabel(list)}`);
|
|
316
432
|
}
|
|
317
433
|
status = 0;
|
|
318
434
|
}
|
|
319
435
|
finally {
|
|
320
436
|
const numbers = pageNumbers ?? pages.map(p => describeSelector(toSelector(p)));
|
|
321
|
-
t.driver.log.push({ ...t.driver.context, cmd: 'fling-to-end', t: t.now(), ms: t.now() - start, status, flings: scrolls, pages: numbers, coastMs, waitMs,
|
|
437
|
+
t.driver.log.push({ ...t.driver.context, cmd: 'fling-to-end', t: t.now(), ms: t.now() - start, status, flings: scrolls, pages: numbers, coastMs, waitMs, landed, unmoved, pauseMs, settled });
|
|
322
438
|
}
|
|
323
439
|
}
|
|
324
440
|
/**
|
|
@@ -344,30 +460,84 @@ export function endChecked(t, list, lastPage, scoped, field, { checkTimeoutMs =
|
|
|
344
460
|
};
|
|
345
461
|
}
|
|
346
462
|
/**
|
|
347
|
-
* Runs cycles (3) end/top measured step pairs with names from stepName(toward, oneBasedCycle).
|
|
348
|
-
*
|
|
349
|
-
*
|
|
463
|
+
* Runs cycles (3) end/top measured step pairs with names from stepName(toward, oneBasedCycle). Each step sends exactly its
|
|
464
|
+
* planned controlled gestures, the same in every run: the end step `scrolls` (3) end gestures, the top step 2, the top one looking
|
|
465
|
+
* before the second (a gesture at the top drags a sheet) and stopping quietly short of the top. Nothing is checked inside a step.
|
|
466
|
+
* After each step's end marker a navigation pass (`navigate`, marked `nav`, evidence only) finishes the way: to the loaded end
|
|
467
|
+
* until a lastPage row is loaded (strokes needed there are counted as the harness defect `navCatchUp`), to the top until `first`
|
|
468
|
+
* shows; the pass ends on lookups and rows at rest, never on frames. Then the end check (lastPage loaded, checkTimeoutMs for the
|
|
469
|
+
* end check) and the top check (first shown). Only cycle one waits for page markers. Shares the frame/row cache across steps and
|
|
470
|
+
* passes. frameTimeoutMs feeds the end frame wait. `fling` configures the top step: its `cap` and `iosTopExtra` no longer widen
|
|
471
|
+
* it. Logs step markers, nav passes and nested helper summaries.
|
|
350
472
|
*/
|
|
351
473
|
export async function scrollCycles(t, { list, first, lastPage, cycles = SCROLL_CYCLES, stepName, fling = {}, ...options }) {
|
|
352
474
|
const cache = {};
|
|
353
475
|
const scoped = options.scoped ?? true;
|
|
476
|
+
const topPlanned = fling.planned ?? TOP_STEP.gestures;
|
|
477
|
+
const lastLoaded = async () => {
|
|
478
|
+
const loaded = await rowLoaded(t, list, lastPage, scoped, undefined, options.field);
|
|
479
|
+
return (loaded === 'stale' ? await rowLoaded(t, list, lastPage, scoped, undefined, options.field) : loaded) === true;
|
|
480
|
+
};
|
|
481
|
+
const firstShown = () => t.isVisible(shownOf(first, scoped));
|
|
354
482
|
for (let i = 1; i <= cycles; i++) {
|
|
355
483
|
await t.waitVisible(list);
|
|
356
|
-
await t.step(stepName('end', i), () => scrollToEnd(t, list, cache, { ...options, pages: i === 1 ? options.pages : [], pageNumbers: i === 1 ? options.pageNumbers : undefined
|
|
357
|
-
|
|
484
|
+
await t.step(stepName('end', i), () => scrollToEnd(t, list, cache, { ...options, pages: i === 1 ? options.pages : [], pageNumbers: i === 1 ? options.pageNumbers : undefined, deferPages: true }));
|
|
485
|
+
// The end step waited out its last gesture's coast (and page): the list is at rest.
|
|
486
|
+
await navigate(t, list, 'end', { until: lastLoaded, row: options.any, field: options.field, scoped, cache, pageWaitMs: options.pageWaitMs ?? PAGE_WAIT_MS, label: selectorLabel(lastPage), catchUp: true, restBefore: false });
|
|
487
|
+
await endChecked(t, list, lastPage, scoped, options.field, options)();
|
|
488
|
+
await t.step(stepName('top', i), () => flingUntilShown(t, list, first, 'top', { ...fling, scoped, cache, row: options.any, cap: topPlanned, planned: topPlanned, iosTopExtra: 0, blind: topPlanned, used: 0, soft: true }));
|
|
489
|
+
await navigate(t, list, 'top', { until: firstShown, row: options.any, field: options.field, scoped, cache, coastMaxMs: fling.coastMaxMs, label: selectorLabel(first) });
|
|
490
|
+
await t.waitAll([{ shown: shownOf(first, scoped) }], fling.checkTimeoutMs ?? CHECK_TIMEOUT_MS);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* A list paged by a marker at its end ("Show more"), measured the D1 way: per page a step of exactly `gestures` controlled
|
|
495
|
+
* gestures toward the end (each after the coast of the one before, nothing looked for inside the step), then a navigation pass
|
|
496
|
+
* outside the step finishes the way to the page's marker, then `onMore`. The measured count is a constant, wherever the list
|
|
497
|
+
* comes to rest (gate runs 10-30: TapIn's hand-written loop looked before every gesture, audience to-end sent 10/10/8).
|
|
498
|
+
*/
|
|
499
|
+
export async function pagedToEnd(t, { list, row, pages, gestures = 1, more, stepName, onMore, field, scoped = true, coastMaxMs }) {
|
|
500
|
+
const cache = {};
|
|
501
|
+
for (let p = 1; p <= pages; p++) {
|
|
502
|
+
const marker = more(p);
|
|
503
|
+
await t.waitVisible(list);
|
|
504
|
+
await t.step(stepName(p), () => flingUntilShown(t, list, marker, 'end', { scoped, cache, row, cap: gestures, planned: gestures, blind: gestures, iosTopExtra: 0, used: 0, soft: true, ...(coastMaxMs ? { coastMaxMs } : {}) }));
|
|
505
|
+
const { childOf: _list, ...anywhere } = toSelector(marker);
|
|
506
|
+
await navigate(t, list, 'end', { until: () => t.isVisible(scoped ? marker : anywhere), row, field, scoped, cache, pageWaitMs: PAGE_WAIT_MS, label: selectorLabel(marker) });
|
|
507
|
+
await onMore(p);
|
|
508
|
+
// The marker's action loads more rows: the next page starts from a list frame read again.
|
|
509
|
+
cache.at = undefined;
|
|
358
510
|
}
|
|
359
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* The measured way back to the top of a list: a step of TOP_STEP.gestures (2) controlled gestures, fewer only when the first row
|
|
514
|
+
* already shows after the first (F7: a gesture at the top drags a sheet), then a navigation pass outside the step to the first row.
|
|
515
|
+
* The same as `scrollCycles`' top half, for lists a journey pages itself (gate runs 10-30: audience to-top sent 6/6/5).
|
|
516
|
+
*/
|
|
517
|
+
export async function topMeasured(t, { list, first, row, stepName, field, scoped = true, coastMaxMs, checkTimeoutMs = CHECK_TIMEOUT_MS }) {
|
|
518
|
+
const cache = {};
|
|
519
|
+
const top = TOP_STEP.gestures;
|
|
520
|
+
await t.waitVisible(list);
|
|
521
|
+
await t.step(stepName, () => flingUntilShown(t, list, first, 'top', { scoped, cache, row, cap: top, planned: top, iosTopExtra: 0, blind: top, used: 0, soft: true, ...(coastMaxMs ? { coastMaxMs } : {}) }));
|
|
522
|
+
await navigate(t, list, 'top', { until: () => t.isVisible(shownOf(first, scoped)), row, field, scoped, cache, label: selectorLabel(first) });
|
|
523
|
+
await t.waitAll([{ shown: shownOf(first, scoped) }], checkTimeoutMs);
|
|
524
|
+
}
|
|
360
525
|
/**
|
|
361
526
|
* Waits settleMs (10000) for target, then taps an optional indicator and/or runs move until visible, at most cap (3)
|
|
362
|
-
* times, coasting on row after each action.
|
|
363
|
-
*
|
|
527
|
+
* times, coasting on row after each action. Outside a measured step this is a navigation pass: marked `nav`, and the list
|
|
528
|
+
* gestures `move` sends are the navigation stroke (the pass has no list to read rows on, so `cap` still ends it). Logs cmd
|
|
529
|
+
* (scroll-to-target) with tapped, swipes, extra above planned (2), status and duration. An initially visible target returns without a summary. missing customizes the failure message.
|
|
364
530
|
*/
|
|
365
|
-
export async function scrollToTarget(t,
|
|
531
|
+
export async function scrollToTarget(t, options) {
|
|
532
|
+
const { target, settleMs = TARGET_SETTLE_MS } = options;
|
|
366
533
|
try {
|
|
367
534
|
await t.waitVisible(target, settleMs);
|
|
368
535
|
return;
|
|
369
536
|
}
|
|
370
537
|
catch { }
|
|
538
|
+
await t.navigationUnlessStep(() => bringToTarget(t, options));
|
|
539
|
+
}
|
|
540
|
+
async function bringToTarget(t, { target, indicator, row, move, cap = LONG_SCROLL_CAP, planned = LONG_SCROLL_PLANNED, cmd = 'scroll-to-target', missing = n => `${selectorLabel(target)} not on screen after ${n} long scrolls` }) {
|
|
371
541
|
const start = t.now();
|
|
372
542
|
let swipes = 0;
|
|
373
543
|
let tapped = false;
|