@keepsnappy/agent 0.1.0-alpha.19 → 0.1.0-alpha.21
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 +1 -1
- package/dist/appium-driver.js +334 -19
- package/dist/keepsnappy.js +577 -237
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,7 +105,7 @@ keepsnappy farm close --state farm-run/keepsnappy-farm.json --bs-build farm-run/
|
|
|
105
105
|
|
|
106
106
|
`keepsnappy farm run` does the whole night in one command (docs/plans/54a): resolve the app (`--app eas:<id>|file|bs://<id>|custom_id:<id>`), `farm open`, an optional BrowserStack Local tunnel (`--tunnel-for localhost:<port>`, repeatable), the device run (`--driver maestro` with `--suite-dir`/`--execute`, or `--driver appium --journey-module FILE` for the built-in Appium driver on iOS or Android, `@keepsnappy/appium-driver`, docs/plans/54b; `--driver-script FILE` still runs your own driver), `farm close`, and cleanup (tunnel stopped, tokens file reset) whatever happened. `--env KEY=VALUE` values may use `{tunnel:<port>}`, `{run_id}`, `{out}`, `{tokens_js}`, `{run_set_id}`. Pre-uploaded `bs://` and `custom_id:` app references pass through without a file upload. Appium uses `--timeout-minutes 80` by default: the driver receives SIGTERM at the timeout, then SIGKILL after `--kill-after-seconds 120` if it has not exited. A timeout is recorded in the failure message, including the terminating signal, and returns exit 1 even if the driver exits zero after SIGTERM. Exit codes as `farm close`; a failed device run is 1. `--dry-run` prints the plan with no request. Needs `KEEPSNAPPY_KEY`, `BROWSERSTACK_USERNAME`, `BROWSERSTACK_ACCESS_KEY` (and `EXPO_TOKEN` for `eas:`).
|
|
107
107
|
|
|
108
|
-
Setup and completion (plan 51 stage 2): `--completion-marker` declares that the flow posts the step marker `run:complete` (end) at the end of every iteration, from the main flow and not from `onFlowComplete`; the API then marks a run without it invalid (`flow-incomplete`). `--driver-version`, `--app-build-id`, `--harness-version`, `--flag-set` go into every run's setup fingerprint; `farm close` adds the device farm's build id from `--bs-build`.
|
|
108
|
+
Setup and completion (plan 51 stage 2): `--completion-marker` declares that the flow posts the step marker `run:complete` (end) at the end of every iteration, from the main flow and not from `onFlowComplete`; the API then marks a run without it invalid (`flow-incomplete`). `--driver-version`, `--app-build-id`, `--harness-version`, `--flag-set` go into every run's setup fingerprint; `farm close` adds the device farm's build id from `--bs-build`. `--plant JSON` records what the build plants against main (a truth runner's planted variant) as the run set's `meta.plant`: a list of `{ place, kind, factor, unit }`, `factor` > 0 in `unit` `budget` (multiples of the frame budget) or `MB`; the quality report shows it as the session's dose.
|
|
109
109
|
|
|
110
110
|
The model a phone is stored under is the hardware code the OS reports (`SM-S911B`, `iPhone17,1`); the marketing name is only shown. With `--platform android|ios`, `--device-provider local|usb|customer|simulator|emulator` (the default `browserstack` needs an explicit model: a farm's phone is not on this machine) and no `--device-model`, `farm open` reads it from the device: Android `adb [-s <serial>] shell getprop ro.product.model`, an iPhone's machine id from `xcrun devicectl list devices`, a simulator's name from `xcrun simctl list devices` (kept with its "Simulator" word, so it never joins a real phone's row). With `--platform ios` and a `--device-id` that `xcrun simctl` lists as a simulator, `farm open` opens the run set as a simulator whatever provider and model were typed: provider `simulator`, the model with the "Simulator" word. It reads nothing it cannot pin to exactly one device (`--device-id` picks one); then `--device-model` is required. A typed `--device-model` is for a device this machine cannot ask (a farm's phone) and is sent as given.
|
|
111
111
|
|
package/dist/appium-driver.js
CHANGED
|
@@ -726,13 +726,311 @@ async function sessionDetail(sessionId, auth, fetchImpl) {
|
|
|
726
726
|
return (await res.json()).automation_session ?? {};
|
|
727
727
|
}
|
|
728
728
|
|
|
729
|
+
// packages/appium-driver/src/gestures.ts
|
|
730
|
+
var FLING = {
|
|
731
|
+
end: [0.85, 0.25],
|
|
732
|
+
top: [0.3, 0.9],
|
|
733
|
+
minTravel: 250,
|
|
734
|
+
contactMs: 150,
|
|
735
|
+
startMargin: 0.1
|
|
736
|
+
};
|
|
737
|
+
function flingStroke(r, toward, unit, safeBottom, start) {
|
|
738
|
+
const [f, t] = FLING[toward];
|
|
739
|
+
const travel = Math.round(Math.max(Math.abs(t - f) * r.height, FLING.minTravel * unit));
|
|
740
|
+
const step = toward === "end" ? -travel : travel;
|
|
741
|
+
const clamp = (y) => Math.min(Math.max(y, 0), safeBottom);
|
|
742
|
+
let from = Math.round(start ?? r.y + r.height * f);
|
|
743
|
+
const below = Math.max(from, from + step) - safeBottom;
|
|
744
|
+
const above = -Math.min(from, from + step);
|
|
745
|
+
if (below > 0)
|
|
746
|
+
from = Math.max(from - below, Math.min(from, Math.round(r.y + r.height * FLING.startMargin)));
|
|
747
|
+
else if (above > 0)
|
|
748
|
+
from = Math.min(from + above, Math.max(from, Math.round(r.y + r.height * (1 - FLING.startMargin))));
|
|
749
|
+
from = clamp(from);
|
|
750
|
+
const to = clamp(from + step);
|
|
751
|
+
return { from, to, travel, clamped: Math.abs(to - from) < travel };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// packages/appium-driver/src/scroll.ts
|
|
755
|
+
var COAST_POLL_MS = 300;
|
|
756
|
+
var COAST_MAX_MS = 3000;
|
|
757
|
+
var FLING_UNMOVED_MS = 4500;
|
|
758
|
+
|
|
759
|
+
class GestureNotTaken extends Error {
|
|
760
|
+
name = "GestureNotTaken";
|
|
761
|
+
}
|
|
762
|
+
async function scrollList(t, spec, toward, { at, cache, hold, mustMove = false, coastMaxMs = COAST_MAX_MS } = {}) {
|
|
763
|
+
if (spec.screen) {
|
|
764
|
+
if (t.driver.platform.name === "ios" && spec.row !== undefined)
|
|
765
|
+
await drawnRowDrag(t, spec, toward);
|
|
766
|
+
else {
|
|
767
|
+
await t.ready();
|
|
768
|
+
await listGesture(t, (await t.frameOf(spec.list, await t.waitVisible(spec.list))).rect, toward);
|
|
769
|
+
}
|
|
770
|
+
return { outcome: "moved", retried: false, sentAt: t.now() };
|
|
771
|
+
}
|
|
772
|
+
if (at)
|
|
773
|
+
return { ...await listGesture(t, at, toward), retried: false };
|
|
774
|
+
const c = cache ?? {};
|
|
775
|
+
const checked = mustMove && spec.row !== undefined;
|
|
776
|
+
const ios = t.driver.platform.name === "ios";
|
|
777
|
+
const held = checked && ios ? hold ?? (c.hold ??= {}) : hold;
|
|
778
|
+
const send = async (first) => {
|
|
779
|
+
c.at = await t.frameOf(spec.list, c.at?.el ?? await t.waitVisible(spec.list));
|
|
780
|
+
if (first && held)
|
|
781
|
+
await holdBeforeScroll(t, spec.row, handle(spec, c), spec.field, held);
|
|
782
|
+
const before = checked && ios ? await heldFrame(t, held) : null;
|
|
783
|
+
const g = await listGesture(t, c.at.rect, toward);
|
|
784
|
+
const after = before ? await heldFrame(t, held) : null;
|
|
785
|
+
const suspect = ios ? before !== null && before === after : g.outcome === "unmoved";
|
|
786
|
+
return { ...g, suspect };
|
|
787
|
+
};
|
|
788
|
+
const first = await send(true);
|
|
789
|
+
if (!checked)
|
|
790
|
+
return { outcome: first.outcome, answer: first.answer, answerMs: first.answerMs, sentAt: first.sentAt, retried: false };
|
|
791
|
+
if (!first.suspect)
|
|
792
|
+
return { outcome: "moved", answer: first.answer, answerMs: first.answerMs, sentAt: first.sentAt, retried: false };
|
|
793
|
+
await waitCoasted(t, spec.row, handle(spec, c), spec.field, coastMaxMs, held);
|
|
794
|
+
const before = await rowsInList(t, spec, c);
|
|
795
|
+
if (!room(t, before, toward))
|
|
796
|
+
return { outcome: "at-edge", answer: first.answer, answerMs: first.answerMs, sentAt: first.sentAt, retried: false };
|
|
797
|
+
const label = listLabel(spec.list);
|
|
798
|
+
const defect = {
|
|
799
|
+
...t.driver.context,
|
|
800
|
+
step: t.driver.context.step ?? null,
|
|
801
|
+
t: t.now(),
|
|
802
|
+
list: label,
|
|
803
|
+
toward,
|
|
804
|
+
reason: "unmoved",
|
|
805
|
+
before: before.ids,
|
|
806
|
+
after: null,
|
|
807
|
+
answer: ios ? null : first.answer,
|
|
808
|
+
answerMs: ios ? null : first.answerMs ?? null
|
|
809
|
+
};
|
|
810
|
+
t.defects.gestureRetry++;
|
|
811
|
+
t.defects.items.push(defect);
|
|
812
|
+
const again = await send(false);
|
|
813
|
+
await waitCoasted(t, spec.row, handle(spec, c), spec.field, coastMaxMs, held);
|
|
814
|
+
const after = await rowsInList(t, spec, c);
|
|
815
|
+
defect.after = after.ids;
|
|
816
|
+
if (after.ids.join(`
|
|
817
|
+
`) === before.ids.join(`
|
|
818
|
+
`))
|
|
819
|
+
throw new GestureNotTaken(`list ${label} did not move after 2 identical gestures (the device took no scroll)`);
|
|
820
|
+
return { outcome: "moved", answer: again.answer, answerMs: again.answerMs, sentAt: again.sentAt, retried: true };
|
|
821
|
+
}
|
|
822
|
+
var handle = (spec, c) => spec.scoped ?? true ? { sel: spec.list, el: c.at?.el } : undefined;
|
|
823
|
+
var listLabel = (list) => {
|
|
824
|
+
const s = toSelector(list);
|
|
825
|
+
return s.id ?? s.text ?? describeSelector(s);
|
|
826
|
+
};
|
|
827
|
+
async function heldFrame(t, hold) {
|
|
828
|
+
if (!hold.el)
|
|
829
|
+
return null;
|
|
830
|
+
try {
|
|
831
|
+
const r = await t.driver.cmd("GET", `/element/${hold.el}/rect`);
|
|
832
|
+
return `${r.x},${r.y},${r.width},${r.height}`;
|
|
833
|
+
} catch {
|
|
834
|
+
hold.el = null;
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
async function rowsInList(t, spec, c) {
|
|
839
|
+
c.at = await t.frameOf(spec.list, c.at?.el ?? await t.waitVisible(spec.list));
|
|
840
|
+
const list = c.at.rect;
|
|
841
|
+
const { childOf: _list, ...sel } = toSelector(spec.row);
|
|
842
|
+
const all = t.driver.platform.locators(sel);
|
|
843
|
+
const android = t.driver.platform.name === "android";
|
|
844
|
+
const call = spec.field === "description" ? /descriptionMatches/ : /textMatches/;
|
|
845
|
+
const own = android && spec.field ? all.filter((l) => call.test(l.value)) : all;
|
|
846
|
+
const attr = sel.text !== undefined ? android ? spec.field === "description" ? "content-desc" : "text" : "label" : android ? "resource-id" : "name";
|
|
847
|
+
const frames = [];
|
|
848
|
+
const inside = [];
|
|
849
|
+
const parent = spec.scoped ?? true ? c.at.el : undefined;
|
|
850
|
+
for (const { using, value } of own.length ? own : all) {
|
|
851
|
+
for (const el of await t.driver.findAll(using, value, parent)) {
|
|
852
|
+
let r;
|
|
853
|
+
try {
|
|
854
|
+
r = await t.driver.cmd("GET", `/element/${el}/rect`);
|
|
855
|
+
} catch {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
if (r.width <= 0 || r.height <= 0)
|
|
859
|
+
continue;
|
|
860
|
+
frames.push(r);
|
|
861
|
+
if (r.y < list.y + list.height && r.y + r.height > list.y)
|
|
862
|
+
inside.push({ y: r.y, el });
|
|
863
|
+
}
|
|
864
|
+
if (frames.length)
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
const ids = [];
|
|
868
|
+
for (const { el } of inside.sort((a, b) => a.y - b.y))
|
|
869
|
+
ids.push(String(await t.driver.attribute(el, attr).catch(() => null) ?? ""));
|
|
870
|
+
return { ids, frames, list };
|
|
871
|
+
}
|
|
872
|
+
function room(t, { frames, list }, toward) {
|
|
873
|
+
if (frames.length === 0)
|
|
874
|
+
return false;
|
|
875
|
+
const android = t.driver.platform.name === "android";
|
|
876
|
+
const bottom = list.y + list.height;
|
|
877
|
+
if (toward === "end") {
|
|
878
|
+
const reach = Math.max(...frames.map((r) => r.y + r.height));
|
|
879
|
+
return android ? reach >= bottom - 1 : reach > bottom;
|
|
880
|
+
}
|
|
881
|
+
const reach = Math.min(...frames.map((r) => r.y));
|
|
882
|
+
return android ? reach <= list.y + 1 : reach < list.y;
|
|
883
|
+
}
|
|
884
|
+
async function listGesture(t, r, toward) {
|
|
885
|
+
await t.ready();
|
|
886
|
+
const android = t.driver.platform.name === "android";
|
|
887
|
+
const safe = await t.driver.safeBottomY();
|
|
888
|
+
const s = flingStroke(r, toward, android ? await t.driver.density() : 1, safe);
|
|
889
|
+
const note = s.clamped ? { clamped: true, travel: Math.abs(s.to - s.from) } : undefined;
|
|
890
|
+
if (android) {
|
|
891
|
+
const logged = t.driver.log.length;
|
|
892
|
+
const sent = t.now();
|
|
893
|
+
const answer = await t.driver.execute("mobile: flingGesture", {
|
|
894
|
+
left: r.x,
|
|
895
|
+
top: Math.min(s.from, s.to),
|
|
896
|
+
width: r.width,
|
|
897
|
+
height: Math.abs(s.to - s.from),
|
|
898
|
+
direction: toward === "end" ? "down" : "up",
|
|
899
|
+
speed: Math.round(1000 * Math.abs(s.to - s.from) / FLING.contactMs)
|
|
900
|
+
});
|
|
901
|
+
const answerMs = t.now() - sent;
|
|
902
|
+
if (note && t.driver.log[logged])
|
|
903
|
+
Object.assign(t.driver.log[logged], note);
|
|
904
|
+
return { outcome: answer === false && answerMs >= FLING_UNMOVED_MS ? "unmoved" : "moved", answer, answerMs, sentAt: t.now() };
|
|
905
|
+
}
|
|
906
|
+
await drag(t, Math.round(r.x + r.width / 2), s, note);
|
|
907
|
+
return { outcome: "moved", sentAt: t.now() };
|
|
908
|
+
}
|
|
909
|
+
var drag = (t, x, s, note) => t.driver.drag({ x, y: s.from }, { x, y: s.to }, FLING.contactMs, note);
|
|
910
|
+
async function drawnRowDrag(t, { list, row, above, missingMessage = "no drawn row to scroll from" }, toward) {
|
|
911
|
+
await t.ready();
|
|
912
|
+
const { using, value } = t.driver.platform.locators(toSelector(row))[0];
|
|
913
|
+
let start = null;
|
|
914
|
+
const drawnAbove = [];
|
|
915
|
+
if (toward === "top") {
|
|
916
|
+
const limit = above ? (await t.driver.cmd("GET", `/element/${await t.waitVisible(toSelector(above))}/rect`)).y : Infinity;
|
|
917
|
+
let lowest = null;
|
|
918
|
+
for (const el of await t.driver.findAll(using, value)) {
|
|
919
|
+
if (await t.driver.cmd("GET", `/element/${el}/attribute/visible`) !== "true")
|
|
920
|
+
continue;
|
|
921
|
+
const r = await t.driver.cmd("GET", `/element/${el}/rect`);
|
|
922
|
+
if (!lowest || r.y > lowest.y)
|
|
923
|
+
lowest = r;
|
|
924
|
+
if (r.y + r.height > limit)
|
|
925
|
+
continue;
|
|
926
|
+
drawnAbove.push(r);
|
|
927
|
+
if (!start || r.y > start.y)
|
|
928
|
+
start = r;
|
|
929
|
+
}
|
|
930
|
+
start ??= lowest;
|
|
931
|
+
} else {
|
|
932
|
+
for (const el of await t.driver.findAll(using, `${value} AND visible == 1`)) {
|
|
933
|
+
const r = await t.driver.cmd("GET", `/element/${el}/rect`);
|
|
934
|
+
if (!start || r.y > start.y)
|
|
935
|
+
start = r;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (!start)
|
|
939
|
+
throw new Error(missingMessage);
|
|
940
|
+
const x = Math.round(start.x + start.width / 2);
|
|
941
|
+
const listEl = await t.driver.findShown(toSelector(list));
|
|
942
|
+
const l = listEl ? await t.driver.cmd("GET", `/element/${listEl}/rect`) : await t.driver.screen();
|
|
943
|
+
let y = Math.round(start.y + start.height / 2);
|
|
944
|
+
if (toward === "end")
|
|
945
|
+
y = Math.min(Math.max(y, l.y + 1), l.y + l.height - 1);
|
|
946
|
+
const safe = await t.driver.safeBottomY();
|
|
947
|
+
let s = flingStroke(l, toward, 1, safe, y);
|
|
948
|
+
if (toward === "top" && s.from !== y && !(drawnAbove.some((r) => r.y <= s.from && s.from < r.y + r.height) && s.from > l.y && s.from < l.y + l.height)) {
|
|
949
|
+
s = { ...s, from: y, to: Math.min(y + s.travel, safe), clamped: true };
|
|
950
|
+
}
|
|
951
|
+
await drag(t, x, s, s.clamped ? { clamped: true, travel: Math.abs(s.to - s.from) } : undefined);
|
|
952
|
+
}
|
|
953
|
+
async function firstDrawn(t, row, listEl, field, scopedOrField) {
|
|
954
|
+
if (!scopedOrField) {
|
|
955
|
+
return t.driver.findShown(toSelector(row));
|
|
956
|
+
}
|
|
957
|
+
const { childOf: _list, ...inList } = toSelector(row);
|
|
958
|
+
const all = t.driver.platform.locators(inList);
|
|
959
|
+
const call = field === "text" ? /textMatches/ : /descriptionMatches/;
|
|
960
|
+
const own = t.driver.platform.name === "android" && field ? all.filter((l) => call.test(l.value)) : all;
|
|
961
|
+
for (const { using, value } of own.length ? own : all) {
|
|
962
|
+
const drawn = t.driver.platform.name === "ios" && using === "-ios predicate string" ? `${value} AND visible == 1` : value;
|
|
963
|
+
const found = await t.driver.find(using, drawn, listEl);
|
|
964
|
+
if (found) {
|
|
965
|
+
return found;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
async function holdBeforeScroll(t, row, list, field, hold) {
|
|
971
|
+
if (!hold || hold.el) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
try {
|
|
975
|
+
hold.el = await firstDrawn(t, row, list?.el, field, Boolean(list || field));
|
|
976
|
+
} catch {
|
|
977
|
+
hold.el = null;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
async function waitCoasted(t, row, list, field, maxMs = COAST_MAX_MS, hold) {
|
|
981
|
+
const until = t.now() + maxMs;
|
|
982
|
+
let el = list?.el;
|
|
983
|
+
let refinds = 0;
|
|
984
|
+
const read = async () => {
|
|
985
|
+
if (hold?.el) {
|
|
986
|
+
try {
|
|
987
|
+
return await t.driver.cmd("GET", `/element/${hold.el}/rect`);
|
|
988
|
+
} catch {
|
|
989
|
+
hold.el = null;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
const found = await firstDrawn(t, row, el, field, Boolean(list || field));
|
|
993
|
+
if (found && hold) {
|
|
994
|
+
hold.el = found;
|
|
995
|
+
}
|
|
996
|
+
return found ? t.driver.cmd("GET", `/element/${found}/rect`) : null;
|
|
997
|
+
};
|
|
998
|
+
let last = null;
|
|
999
|
+
let reads = 0;
|
|
1000
|
+
for (;; ) {
|
|
1001
|
+
let r = null;
|
|
1002
|
+
reads++;
|
|
1003
|
+
try {
|
|
1004
|
+
r = await read();
|
|
1005
|
+
} catch {
|
|
1006
|
+
r = null;
|
|
1007
|
+
}
|
|
1008
|
+
if (r === null && list && refinds < 2) {
|
|
1009
|
+
refinds++;
|
|
1010
|
+
el = await t.waitVisible(list.sel);
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
const frame = r && `${r.x},${r.y},${r.width},${r.height}`;
|
|
1014
|
+
if (frame && frame === last)
|
|
1015
|
+
return true;
|
|
1016
|
+
if (t.now() >= until && reads >= 2)
|
|
1017
|
+
return false;
|
|
1018
|
+
last = frame;
|
|
1019
|
+
await t.driver.sleep(COAST_POLL_MS);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// packages/appium-driver/src/lists.ts
|
|
1024
|
+
var LOOK_MS = 1000;
|
|
1025
|
+
var LOOK_POLL_MS = 100;
|
|
1026
|
+
|
|
729
1027
|
// packages/appium-driver/src/version.ts
|
|
730
1028
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
731
1029
|
import { dirname, join, relative, resolve } from "node:path";
|
|
732
1030
|
import { fileURLToPath } from "node:url";
|
|
733
1031
|
var DRIVER_NAME = "appium";
|
|
734
1032
|
function driverVersion() {
|
|
735
|
-
const built = "0.1.0-alpha.
|
|
1033
|
+
const built = "0.1.0-alpha.21";
|
|
736
1034
|
if (built)
|
|
737
1035
|
return built;
|
|
738
1036
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -754,9 +1052,6 @@ var MARKERS_PER_REQUEST = 500;
|
|
|
754
1052
|
var MARKER_TRIES = 3;
|
|
755
1053
|
var HOST_CLOCK_TRIPS = 6;
|
|
756
1054
|
var RETRY_BASE_MS = 1000;
|
|
757
|
-
var FLING_MS = 80;
|
|
758
|
-
var FLING_FROM = 0.8;
|
|
759
|
-
var FLING_TO = 0.3;
|
|
760
1055
|
var FLING_TIMEOUT_MS = 30000;
|
|
761
1056
|
var STALE_REFINDS = 3;
|
|
762
1057
|
var FRAME_POLL_MS = 50;
|
|
@@ -806,7 +1101,10 @@ async function sendMarkers(fetchImpl, api, markers, opts = {}) {
|
|
|
806
1101
|
const url = `${base}/v1/markers`;
|
|
807
1102
|
const version = (opts.driverVersion ?? driverVersion)();
|
|
808
1103
|
const reported = opts.reported ?? new Set;
|
|
809
|
-
const driverField =
|
|
1104
|
+
const driverField = (token) => {
|
|
1105
|
+
const defects = opts.defects?.(token);
|
|
1106
|
+
return { driver: { name: DRIVER_NAME, ...version ? { version } : {}, ...opts.deviceUnit ? { unit: opts.deviceUnit.slice(0, 200) } : {}, ...opts.capture ? { capture: opts.capture } : {}, ...defects ? { defects } : {} } };
|
|
1107
|
+
};
|
|
810
1108
|
let sent = 0;
|
|
811
1109
|
for (const [token, list] of byToken) {
|
|
812
1110
|
const trips = [];
|
|
@@ -815,7 +1113,7 @@ async function sendMarkers(fetchImpl, api, markers, opts = {}) {
|
|
|
815
1113
|
return carried.length ? { ...body, clock: carried } : body;
|
|
816
1114
|
};
|
|
817
1115
|
const send = async (body) => {
|
|
818
|
-
const answer = await postWithRetry(fetchImpl, url, token, { ...withClock(body), ...reported.has(token) ? {} : driverField }, sleep, now);
|
|
1116
|
+
const answer = await postWithRetry(fetchImpl, url, token, { ...withClock(body), ...reported.has(token) ? {} : driverField(token) }, sleep, now);
|
|
819
1117
|
if (answer && answer.status < 300)
|
|
820
1118
|
reported.add(token);
|
|
821
1119
|
if (answer?.trip)
|
|
@@ -849,7 +1147,7 @@ async function sendMarkers(fetchImpl, api, markers, opts = {}) {
|
|
|
849
1147
|
for (const token of opts.tokens ?? []) {
|
|
850
1148
|
if (byToken.has(token) || reported.has(token) || !token || !api)
|
|
851
1149
|
continue;
|
|
852
|
-
const answer = await postWithRetry(fetchImpl, `${base}/v1/clock`, token, { source: "host", samples: [], ...driverField }, sleep, now);
|
|
1150
|
+
const answer = await postWithRetry(fetchImpl, `${base}/v1/clock`, token, { source: "host", samples: [], ...driverField(token) }, sleep, now);
|
|
853
1151
|
reported.add(token);
|
|
854
1152
|
if (!answer || answer.status >= 300)
|
|
855
1153
|
opts.note?.(`driver report for a run not accepted (${answer?.status ?? "no answer"}); not tried again`);
|
|
@@ -895,6 +1193,14 @@ var RESCUE_EVERY_MS = 3000;
|
|
|
895
1193
|
var noop = async () => {
|
|
896
1194
|
return;
|
|
897
1195
|
};
|
|
1196
|
+
var RETRY_COMMAND = /-retry$/;
|
|
1197
|
+
function harnessDefects(commands, gestureRetries) {
|
|
1198
|
+
const out = { gestureRetry: gestureRetries };
|
|
1199
|
+
for (const c of commands)
|
|
1200
|
+
if (RETRY_COMMAND.test(c.cmd))
|
|
1201
|
+
out[c.cmd] = (out[c.cmd] ?? 0) + 1;
|
|
1202
|
+
return out;
|
|
1203
|
+
}
|
|
898
1204
|
|
|
899
1205
|
class Session {
|
|
900
1206
|
driver;
|
|
@@ -1142,16 +1448,21 @@ class Session {
|
|
|
1142
1448
|
if (!await this.isVisible(target))
|
|
1143
1449
|
throw new Error(`${describeSelector(toSelector(target))} not visible after ${times} swipes from ${describeSelector(toSelector(from))}`);
|
|
1144
1450
|
}
|
|
1145
|
-
async scrollUntil(sel, { direction = "down", timeoutMs = 15000
|
|
1451
|
+
async scrollUntil(sel, { direction = "down", timeoutMs = 15000 } = {}) {
|
|
1146
1452
|
const s = toSelector(sel);
|
|
1147
1453
|
const deadline = this.now() + timeoutMs;
|
|
1148
|
-
|
|
1454
|
+
let lookUntil = 0;
|
|
1149
1455
|
for (;; ) {
|
|
1150
1456
|
if (await this.driver.findShown(s, true))
|
|
1151
1457
|
return;
|
|
1152
1458
|
if (this.now() > deadline)
|
|
1153
1459
|
throw new Error(`${describeSelector(s)} not scrolled into view within ${timeoutMs} ms`);
|
|
1154
|
-
|
|
1460
|
+
if (this.now() < lookUntil) {
|
|
1461
|
+
await this.driver.sleep(LOOK_POLL_MS);
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
await scrollList(this, {}, direction === "down" ? "end" : "top", { at: await this.driver.screen() });
|
|
1465
|
+
lookUntil = this.now() + LOOK_MS;
|
|
1155
1466
|
}
|
|
1156
1467
|
}
|
|
1157
1468
|
async frameOf(sel, el) {
|
|
@@ -1186,12 +1497,10 @@ class Session {
|
|
|
1186
1497
|
at = now;
|
|
1187
1498
|
}
|
|
1188
1499
|
}
|
|
1189
|
-
async flingIn(r, toward
|
|
1190
|
-
|
|
1191
|
-
const x = Math.round(r.x + r.width / 2);
|
|
1192
|
-
await this.driver.drag({ x, y: Math.round(r.y + r.height * from) }, { x, y: Math.round(r.y + r.height * to) }, durationMs);
|
|
1500
|
+
async flingIn(r, toward) {
|
|
1501
|
+
await scrollList(this, {}, toward, { at: r });
|
|
1193
1502
|
}
|
|
1194
|
-
async flingTimes(list, times, toward, {
|
|
1503
|
+
async flingTimes(list, times, toward, { timeoutMs = FLING_TIMEOUT_MS } = {}) {
|
|
1195
1504
|
if (!Number.isInteger(times) || times < 1)
|
|
1196
1505
|
throw new Error(`flingTimes needs a whole number of flings >= 1, got ${times}`);
|
|
1197
1506
|
await this.ready();
|
|
@@ -1202,13 +1511,13 @@ class Session {
|
|
|
1202
1511
|
try {
|
|
1203
1512
|
const { rect } = await this.settledFrame(list, timeoutMs);
|
|
1204
1513
|
for (;flings < times; flings++)
|
|
1205
|
-
await this.flingIn(rect, toward
|
|
1514
|
+
await this.flingIn(rect, toward);
|
|
1206
1515
|
status = 0;
|
|
1207
1516
|
} finally {
|
|
1208
1517
|
this.driver.log.push({ ...this.driver.context, cmd: `fling-${toward}-times`, t: this.now(), ms: this.now() - start, status, flings, refinds: this.refinds - refindsBefore });
|
|
1209
1518
|
}
|
|
1210
1519
|
}
|
|
1211
|
-
async flingUntil(list, target, toward, { blind = 0,
|
|
1520
|
+
async flingUntil(list, target, toward, { blind = 0, timeoutMs = FLING_TIMEOUT_MS } = {}) {
|
|
1212
1521
|
await this.ready();
|
|
1213
1522
|
const start = this.now();
|
|
1214
1523
|
const refindsBefore = this.refinds;
|
|
@@ -1220,7 +1529,7 @@ class Session {
|
|
|
1220
1529
|
const fling = async () => {
|
|
1221
1530
|
if (flings > 0)
|
|
1222
1531
|
at = await this.frameOf(list, at.el);
|
|
1223
|
-
await this.flingIn(at.rect, toward
|
|
1532
|
+
await this.flingIn(at.rect, toward);
|
|
1224
1533
|
flings++;
|
|
1225
1534
|
};
|
|
1226
1535
|
while (flings < blind)
|
|
@@ -1288,6 +1597,7 @@ class Session {
|
|
|
1288
1597
|
this.token = token;
|
|
1289
1598
|
if (token)
|
|
1290
1599
|
this.opened.add(token);
|
|
1600
|
+
this.runFrom = { commands: this.driver.log.length, gestureRetries: this.defects.gestureRetry };
|
|
1291
1601
|
this.pendingReady = null;
|
|
1292
1602
|
await this.relaunch();
|
|
1293
1603
|
await this.waitVisible(home, 30000);
|
|
@@ -1297,12 +1607,17 @@ class Session {
|
|
|
1297
1607
|
}
|
|
1298
1608
|
opened = new Set;
|
|
1299
1609
|
told = new Set;
|
|
1610
|
+
runFrom = null;
|
|
1611
|
+
runDefects = new Map;
|
|
1300
1612
|
lastFlush = null;
|
|
1301
1613
|
flushSeq = 0;
|
|
1302
1614
|
async endRun() {
|
|
1303
1615
|
if (this.markers.some((m) => m.phase === "start"))
|
|
1304
1616
|
await this.memRead(await this.gc(true));
|
|
1305
|
-
|
|
1617
|
+
if (this.runFrom && this.token)
|
|
1618
|
+
this.runDefects.set(this.token, harnessDefects(this.driver.log.slice(this.runFrom.commands), this.defects.gestureRetry - this.runFrom.gestureRetries));
|
|
1619
|
+
this.runFrom = null;
|
|
1620
|
+
await sendMarkers(this.fetch, this.api, this.markers, { sleep: (ms) => this.driver.sleep(ms), now: () => this.now(), deviceUnit: this.driver.deviceUnit, capture: this.capture, defects: (token) => this.runDefects.get(token), tokens: [...this.opened], reported: this.told, note: (line) => this.driver.log.push({ ...this.driver.context, cmd: "driver-report", t: this.now(), ms: 0, status: 1, answer: line }) });
|
|
1306
1621
|
const evidence = { linkAt: this.now(), resentAt: null, ackAt: null, gaveUpAt: null };
|
|
1307
1622
|
this.lastFlush = evidence;
|
|
1308
1623
|
const acks = new Set;
|