@keepsnappy/agent 0.1.0-alpha.14 → 0.1.0-alpha.16
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 +33 -4
- package/dist/appium-driver.js +68 -17
- package/dist/keepsnappy.js +1317 -264
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,10 +103,12 @@ keepsnappy farm open --api https://keepsnappy-api-….run.app --branch main --co
|
|
|
103
103
|
keepsnappy farm close --state farm-run/keepsnappy-farm.json --bs-build farm-run/browserstack.ios.json --settle-seconds 60 --wait
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
`keepsnappy farm run` does the whole night in one command (docs/plans/54a): resolve the app (`--app eas:<id>|file|bs
|
|
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
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`.
|
|
109
109
|
|
|
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
|
+
|
|
110
112
|
`farm open` also sends `journeys[]`: with `--config keepsnappy.yml` and/or `--flows <files|dirs|globs,...>` each `--journeys` name is matched to the flow file named like it (title, description, source path as for `keepsnappy run`); without them, keys only.
|
|
111
113
|
|
|
112
114
|
`farm open` makes a run set (`source: farm`, the device of `--device-model/--device-os`: the journey history is keyed by
|
|
@@ -176,25 +178,31 @@ same words (`packages/mcp/README.md`).
|
|
|
176
178
|
```sh
|
|
177
179
|
export KEEPSNAPPY_KEY=pl_rk_… # a project key with read scope (a CI key with run scope works too)
|
|
178
180
|
keepsnappy findings --branch feat/keepsnappy-integration # what got worse or better on a PR, worst first
|
|
181
|
+
keepsnappy status --branch feat/keepsnappy-integration # is my PR ok? one answer: blocked / nothing blocks / ok, per journey × phone, ids of the worst findings, links
|
|
179
182
|
keepsnappy findings --main --journey live-ui --phone "iPhone 15"
|
|
180
183
|
keepsnappy findings --state all --json # also the resolved and not judged ones, as the API's JSON
|
|
181
184
|
keepsnappy findings --state all --offset 200 # the next page (at most 200 per answer)
|
|
182
185
|
keepsnappy finding f_0123456789abcdef # one finding with its evidence
|
|
183
186
|
keepsnappy finding f_0123456789abcdef --prompt | pbcopy # only the hand-off for a coding agent, ending with its id and page
|
|
184
187
|
keepsnappy compare <runA> <runB> # two runs (ids or external ids), window by window, no statistics
|
|
185
|
-
keepsnappy history live-ui
|
|
188
|
+
keepsnappy history live-ui press:live.chat.send # one window on main, run by run (exact place id)
|
|
186
189
|
keepsnappy commits a1b2c3d c3d4e5f [--branch main] # the measured commits between two points
|
|
190
|
+
keepsnappy places --journey live-ui --phone "iPhone 15" # every place of the newest main session from its stored statistics, no verdict
|
|
191
|
+
keepsnappy places --run-set <id> --measure commit_max_ms --sort p90 --limit 10
|
|
187
192
|
keepsnappy layers # which layer answers which question, and its certificate
|
|
193
|
+
keepsnappy findings --branch feat/x --short # the short form: what decides, no evidence detail (also status, finding, history, places, main, trend)
|
|
194
|
+
keepsnappy trend live-ui Tap: live chat send --phone "iPhone 15" # the words after the journey are one window name; more windows with repeated --window
|
|
188
195
|
```
|
|
189
196
|
|
|
190
|
-
Flags: `--api URL` (or `KEEPSNAPPY_API`, default the hosted API), `--key-env NAME` (default `KEEPSNAPPY_KEY`), `--json`.
|
|
197
|
+
Flags: `--api URL` (or `KEEPSNAPPY_API`, default the hosted API), `--key-env NAME` (default `KEEPSNAPPY_KEY`), `--json`, `--short`.
|
|
198
|
+
`--short` prints the same text with the evidence detail left out (the MCP tools' `form: "short"`); its last line says what was left out, and the MCP tool descriptions list it per answer. `--json` is the same with or without it.
|
|
191
199
|
A finding's id is stable: the same window, measure, journey and phone model keep the id across commits and PRs, so
|
|
192
200
|
`keepsnappy finding <id>` after a fix tells whether it is resolved. `--state` (default `present`) filters on the state on
|
|
193
201
|
the asked branch: `present` (in the newest run set), `resolved` (the newest run set judged the measure: No change, or the
|
|
194
202
|
other way), `not-judged` (the newest run set did not judge it: not measured, unstable, not enough data; it may still be
|
|
195
203
|
there). A list starts with whether and when the branch was measured ("No KeepSnappy measurement on branch X", or the
|
|
196
204
|
newest measured commit, its word and runs), so an empty list on an unmeasured branch does not read as clean. `commits`
|
|
197
|
-
lists each measured commit once, with how many sessions measured it. Words come from the one word table (Worse / Warning / Better …), "Worse (not blocking)" when the
|
|
205
|
+
lists each measured commit once, with how many sessions measured it. `places` lists the places of one session (a run set, or the newest closed one of a journey on a phone and branch) with each measure's median, spread and the components with the most own time, ranked by `--measure` and `--sort`; a session without stored statistics says `not computed`, never zero. Words come from the one word table (Worse / Warning / Better …), "Worse (not blocking)" when the
|
|
198
206
|
branch was measured differently from main.
|
|
199
207
|
|
|
200
208
|
## Output layout
|
|
@@ -257,3 +265,24 @@ Memory slopes (Theil-Sen over the cycle index, 3+ cycles of `--mem-window`, defa
|
|
|
257
265
|
verdicts, profile-analysis, mcp), so both work from a clean checkout.
|
|
258
266
|
|
|
259
267
|
Only `keepsnappy` and `keepsnappy.yml` / `keepsnappy.yaml` are supported. Configure `KEEPSNAPPY_*` variables. Explicit `--key-env`, `project.key_env`, and `--config` remain arbitrary user-supplied names and paths.
|
|
268
|
+
|
|
269
|
+
### Source maps from an EAS build
|
|
270
|
+
|
|
271
|
+
In an `eas-build-on-success` hook, run:
|
|
272
|
+
|
|
273
|
+
```sh
|
|
274
|
+
npx @keepsnappy/agent sourcemaps upload --eas
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`--eas` runs only when `EAS_BUILD_PROFILE=e2e`. It uses `EAS_BUILD_GIT_COMMIT_HASH` as the build commit and `EAS_BUILD_PLATFORM` to select the map relative to the working directory:
|
|
278
|
+
|
|
279
|
+
| Platform | Map | Kind |
|
|
280
|
+
| --- | --- | --- |
|
|
281
|
+
| `ios` | `main.jsbundle.packager.map` | `packager` |
|
|
282
|
+
| `android` | `android/app/build/generated/sourcemaps/react/release/index.android.bundle.map` | `hermes` |
|
|
283
|
+
|
|
284
|
+
The iOS packager map preserves the positions used by CPU profiles; Android profiles use the composed Hermes map. The upload flavour defaults to `e2e`.
|
|
285
|
+
|
|
286
|
+
Set `KEEPSNAPPY_KEY` or `KEEPSNAPPY_CI_KEY` and `KEEPSNAPPY_API` or `EXPO_PUBLIC_KEEPSNAPPY_INGEST_URL`. The usual key/API names take precedence over the EAS hook names. Explicit `--key-env`, `--api`/`--ingest-url`, `--build`, `--platform`, `--kind`, `--flavour` and map files override these defaults. An explicit `--key-env` uses only that variable.
|
|
287
|
+
|
|
288
|
+
A missing key, API URL, commit or map, an unsupported platform, or a profile other than `e2e` produces one skip line and exits 0. There is no default API URL in EAS mode. Upload, map parsing and argument failures also exit 0 with a generic failure line so the application build stays successful and error details cannot expose credentials. Ordinary `sourcemaps upload` keeps its existing validation and failure behavior. Source text (`sourcesContent`) is stripped before upload in both modes.
|
package/dist/appium-driver.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// packages/appium-driver/src/load.ts
|
|
4
|
-
import { isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { isAbsolute, resolve as resolve2 } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
|
|
7
7
|
// packages/appium-driver/src/run.ts
|
|
8
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
8
|
+
import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { join as join2 } from "node:path";
|
|
10
10
|
import { parseArgs } from "node:util";
|
|
11
11
|
|
|
12
12
|
// packages/appium-driver/src/browserstack.ts
|
|
@@ -314,6 +314,7 @@ class Driver {
|
|
|
314
314
|
now;
|
|
315
315
|
sleep;
|
|
316
316
|
sessionId = null;
|
|
317
|
+
deviceUnit;
|
|
317
318
|
context = {};
|
|
318
319
|
viewport = null;
|
|
319
320
|
rects = new Map;
|
|
@@ -348,9 +349,14 @@ class Driver {
|
|
|
348
349
|
}
|
|
349
350
|
return value;
|
|
350
351
|
}
|
|
352
|
+
async attribute(el, name) {
|
|
353
|
+
const value = await this.cmd("GET", `/element/${el}/attribute/${name}`);
|
|
354
|
+
return value === this.platform.absentAttributeText ? null : value;
|
|
355
|
+
}
|
|
351
356
|
async start(capabilities) {
|
|
352
357
|
const value = await this.cmd("POST", "/session", { capabilities: { alwaysMatch: capabilities } });
|
|
353
358
|
this.sessionId = value.sessionId;
|
|
359
|
+
this.deviceUnit = [value.capabilities?.udid, value.capabilities?.deviceUDID].find((unit) => typeof unit === "string" && unit.length > 0);
|
|
354
360
|
return value;
|
|
355
361
|
}
|
|
356
362
|
execute(script, arg) {
|
|
@@ -427,7 +433,7 @@ class Driver {
|
|
|
427
433
|
break;
|
|
428
434
|
let resourceId;
|
|
429
435
|
try {
|
|
430
|
-
resourceId = String(await this.
|
|
436
|
+
resourceId = String(await this.attribute(el, "resource-id") ?? "");
|
|
431
437
|
} catch (e) {
|
|
432
438
|
if (e instanceof WdError && e.wdError === "stale element reference")
|
|
433
439
|
continue;
|
|
@@ -530,6 +536,7 @@ var IOS = {
|
|
|
530
536
|
"appium:newCommandTimeout": NEW_COMMAND_TIMEOUT_S,
|
|
531
537
|
"appium:waitForIdleTimeout": NO_WAIT_MS
|
|
532
538
|
}),
|
|
539
|
+
absentAttributeText: null,
|
|
533
540
|
settings: { animationCoolOffTimeout: NO_WAIT_MS, waitForIdleTimeout: NO_WAIT_MS, shouldUseCompactResponses: true },
|
|
534
541
|
locators: (sel) => [locator(sel)],
|
|
535
542
|
keyboardShown: async (driver) => Boolean(await driver.findShown(id("Return"))),
|
|
@@ -548,7 +555,7 @@ async function iosCommand(driver, link, cmd, n, state, waitMs) {
|
|
|
548
555
|
for (;; ) {
|
|
549
556
|
const el = await driver.find("-ios predicate string", want);
|
|
550
557
|
if (el)
|
|
551
|
-
return driver.
|
|
558
|
+
return driver.attribute(el, "label").then(String).catch(() => {
|
|
552
559
|
driver.log.push({ ...driver.context, cmd: `${cmd}-ack-label`, t: driver.now(), ms: 0, status: 1 });
|
|
553
560
|
return `${cmd}:${n} ok`;
|
|
554
561
|
});
|
|
@@ -568,6 +575,7 @@ var ANDROID = {
|
|
|
568
575
|
"appium:autoGrantPermissions": true,
|
|
569
576
|
"appium:newCommandTimeout": NEW_COMMAND_TIMEOUT_S
|
|
570
577
|
}),
|
|
578
|
+
absentAttributeText: "null",
|
|
571
579
|
settings: { waitForIdleTimeout: NO_WAIT_MS, actionAcknowledgmentTimeout: NO_WAIT_MS, waitForSelectorTimeout: NO_WAIT_MS, enableMultiWindows: true, ignoreUnimportantViews: true },
|
|
572
580
|
locators: androidLocators,
|
|
573
581
|
appArg: (appId) => ({ appId }),
|
|
@@ -635,7 +643,7 @@ var HUB = "https://hub-cloud.browserstack.com/wd/hub";
|
|
|
635
643
|
var BS_API = "https://api-cloud.browserstack.com/app-automate";
|
|
636
644
|
var basicAuth = (user, key) => `Basic ${Buffer.from(`${user}:${key}`).toString("base64")}`;
|
|
637
645
|
async function resolveApp(app, customId, auth, fetchImpl, platform = IOS) {
|
|
638
|
-
if (app.startsWith("bs://"))
|
|
646
|
+
if (app.startsWith("bs://") || app.startsWith("custom_id:"))
|
|
639
647
|
return app;
|
|
640
648
|
const headers = { Authorization: auth };
|
|
641
649
|
if (customId) {
|
|
@@ -679,6 +687,29 @@ async function sessionDetail(sessionId, auth, fetchImpl) {
|
|
|
679
687
|
return (await res.json()).automation_session ?? {};
|
|
680
688
|
}
|
|
681
689
|
|
|
690
|
+
// packages/appium-driver/src/version.ts
|
|
691
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
692
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
693
|
+
import { fileURLToPath } from "node:url";
|
|
694
|
+
var DRIVER_NAME = "appium";
|
|
695
|
+
function driverVersion() {
|
|
696
|
+
const built = "0.1.0-alpha.16";
|
|
697
|
+
if (built)
|
|
698
|
+
return built;
|
|
699
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
700
|
+
for (let i = 0;i < 3; i++, dir = dirname(dir)) {
|
|
701
|
+
const file = join(dir, "package.json");
|
|
702
|
+
if (!existsSync(file))
|
|
703
|
+
continue;
|
|
704
|
+
try {
|
|
705
|
+
const pkg = JSON.parse(readFileSync(file, "utf8"));
|
|
706
|
+
if (pkg.name === "@keepsnappy/appium-driver" && pkg.version)
|
|
707
|
+
return pkg.version;
|
|
708
|
+
} catch {}
|
|
709
|
+
}
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
|
|
682
713
|
// packages/appium-driver/src/session.ts
|
|
683
714
|
var MARKERS_PER_REQUEST = 500;
|
|
684
715
|
var MARKER_TRIES = 3;
|
|
@@ -734,6 +765,9 @@ async function sendMarkers(fetchImpl, api, markers, opts = {}) {
|
|
|
734
765
|
}
|
|
735
766
|
const base = api.replace(/\/+$/, "");
|
|
736
767
|
const url = `${base}/v1/markers`;
|
|
768
|
+
const version = (opts.driverVersion ?? driverVersion)();
|
|
769
|
+
const reported = opts.reported ?? new Set;
|
|
770
|
+
const driverField = { driver: { name: DRIVER_NAME, ...version ? { version } : {}, ...opts.deviceUnit ? { unit: opts.deviceUnit.slice(0, 200) } : {} } };
|
|
737
771
|
let sent = 0;
|
|
738
772
|
for (const [token, list] of byToken) {
|
|
739
773
|
const trips = [];
|
|
@@ -742,7 +776,9 @@ async function sendMarkers(fetchImpl, api, markers, opts = {}) {
|
|
|
742
776
|
return carried.length ? { ...body, clock: carried } : body;
|
|
743
777
|
};
|
|
744
778
|
const send = async (body) => {
|
|
745
|
-
const answer = await postWithRetry(fetchImpl, url, token, withClock(body), sleep, now);
|
|
779
|
+
const answer = await postWithRetry(fetchImpl, url, token, { ...withClock(body), ...reported.has(token) ? {} : driverField }, sleep, now);
|
|
780
|
+
if (answer && answer.status < 300)
|
|
781
|
+
reported.add(token);
|
|
746
782
|
if (answer?.trip)
|
|
747
783
|
trips.push(answer.trip);
|
|
748
784
|
return answer;
|
|
@@ -771,6 +807,14 @@ async function sendMarkers(fetchImpl, api, markers, opts = {}) {
|
|
|
771
807
|
if (trips.length)
|
|
772
808
|
await post(fetchImpl, `${base}/v1/clock`, token, { source: "host", samples: trips.splice(0) }, now);
|
|
773
809
|
}
|
|
810
|
+
for (const token of opts.tokens ?? []) {
|
|
811
|
+
if (byToken.has(token) || reported.has(token) || !token || !api)
|
|
812
|
+
continue;
|
|
813
|
+
const answer = await postWithRetry(fetchImpl, `${base}/v1/clock`, token, { source: "host", samples: [], ...driverField }, sleep, now);
|
|
814
|
+
reported.add(token);
|
|
815
|
+
if (!answer || answer.status >= 300)
|
|
816
|
+
opts.note?.(`driver report for a run not accepted (${answer?.status ?? "no answer"}); not tried again`);
|
|
817
|
+
}
|
|
774
818
|
return sent;
|
|
775
819
|
}
|
|
776
820
|
var SDK_FLUSH_WORST_CASE_MS = 315000;
|
|
@@ -778,6 +822,7 @@ var FLUSH_MARGIN_MS = 30000;
|
|
|
778
822
|
var FLUSH_WAIT_MS = SDK_FLUSH_WORST_CASE_MS + FLUSH_MARGIN_MS;
|
|
779
823
|
var FLUSH_POLL_MS = 500;
|
|
780
824
|
var FLUSH_RESEND_MS = 90000;
|
|
825
|
+
var ACTIVATION_SETTLE_MS = 2000;
|
|
781
826
|
var FLUSH_NOT_ACKNOWLEDGED = "not acknowledged (re-sent once)";
|
|
782
827
|
var FLUSH_UNREADABLE = "not acknowledged (unreadable answer)";
|
|
783
828
|
var FLUSH_STATUSES = new Set(["ok", "error", "off"]);
|
|
@@ -1179,17 +1224,23 @@ class Session {
|
|
|
1179
1224
|
}
|
|
1180
1225
|
async beginRun({ token, args = "", home }) {
|
|
1181
1226
|
this.token = token;
|
|
1227
|
+
if (token)
|
|
1228
|
+
this.opened.add(token);
|
|
1182
1229
|
this.pendingReady = null;
|
|
1183
1230
|
await this.relaunch();
|
|
1184
1231
|
await this.waitVisible(home, 30000);
|
|
1185
|
-
|
|
1232
|
+
const alert = await this.openLink(activationLink(this.scheme, token, args));
|
|
1233
|
+
await this.driver.sleep(ACTIVATION_SETTLE_MS);
|
|
1234
|
+
return alert;
|
|
1186
1235
|
}
|
|
1236
|
+
opened = new Set;
|
|
1237
|
+
told = new Set;
|
|
1187
1238
|
lastFlush = null;
|
|
1188
1239
|
flushSeq = 0;
|
|
1189
1240
|
async endRun() {
|
|
1190
1241
|
if (this.markers.some((m) => m.phase === "start"))
|
|
1191
1242
|
await this.memRead(await this.gc(true));
|
|
1192
|
-
await sendMarkers(this.fetch, this.api, this.markers, { sleep: (ms) => this.driver.sleep(ms), now: () => this.now() });
|
|
1243
|
+
await sendMarkers(this.fetch, this.api, this.markers, { sleep: (ms) => this.driver.sleep(ms), now: () => this.now(), deviceUnit: this.driver.deviceUnit, 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 }) });
|
|
1193
1244
|
const evidence = { linkAt: this.now(), resentAt: null, ackAt: null, gaveUpAt: null };
|
|
1194
1245
|
this.lastFlush = evidence;
|
|
1195
1246
|
const acks = new Set;
|
|
@@ -1226,8 +1277,8 @@ class Session {
|
|
|
1226
1277
|
if (!el && flush.fallback && poll % FLUSH_FALLBACK_EVERY === 0)
|
|
1227
1278
|
el = await this.findFallback(flush.fallback);
|
|
1228
1279
|
if (el) {
|
|
1229
|
-
const label = await this.driver.
|
|
1230
|
-
const raw = await this.driver.
|
|
1280
|
+
const label = await this.driver.attribute(el, flush.statusAttribute).catch(() => null);
|
|
1281
|
+
const raw = await this.driver.attribute(el, flush.detailAttribute).then((value) => ({ value }), () => null);
|
|
1231
1282
|
const detail = typeof raw?.value === "string" ? raw.value : null;
|
|
1232
1283
|
const echoed = detail === null ? null : ACK_ECHO.exec(detail);
|
|
1233
1284
|
if (raw === null) {
|
|
@@ -1322,8 +1373,8 @@ function writeAtomic(file, text) {
|
|
|
1322
1373
|
renameSync(tmp, file);
|
|
1323
1374
|
}
|
|
1324
1375
|
async function captureFailure(driver, out) {
|
|
1325
|
-
const source = driver.cmd("GET", "/source").then((s) => void (typeof s === "string" && writeFileSync(
|
|
1326
|
-
const screen = driver.cmd("GET", "/screenshot").then((png) => void (typeof png === "string" && writeFileSync(
|
|
1376
|
+
const source = driver.cmd("GET", "/source").then((s) => void (typeof s === "string" && writeFileSync(join2(out, "failure-source.xml"), s)), () => null);
|
|
1377
|
+
const screen = driver.cmd("GET", "/screenshot").then((png) => void (typeof png === "string" && writeFileSync(join2(out, "failure-screen.png"), Buffer.from(png, "base64"))), () => null);
|
|
1327
1378
|
await Promise.all([source, screen]);
|
|
1328
1379
|
}
|
|
1329
1380
|
async function runDriver(argv, module, deps = realRunDeps()) {
|
|
@@ -1349,7 +1400,7 @@ async function runDriver(argv, module, deps = realRunDeps()) {
|
|
|
1349
1400
|
const out = String(a.out);
|
|
1350
1401
|
const auth = basicAuth(user, key);
|
|
1351
1402
|
mkdirSync(out, { recursive: true });
|
|
1352
|
-
const { api, tokens } = parseTokensJs(
|
|
1403
|
+
const { api, tokens } = parseTokensJs(readFileSync2(String(a["tokens-js"]), "utf8"));
|
|
1353
1404
|
const log = [];
|
|
1354
1405
|
const driver = new Driver({ hub: HUB, log, headers: { Authorization: auth }, bundleId: module.app.bundleId, platform, fetch: deps.fetch, now: deps.now, sleep: deps.sleep });
|
|
1355
1406
|
const t = new Session({ driver, scheme: module.app.scheme, fetch: deps.fetch, api, actionGc: a["action-gc"] === "on", endGc: a["end-gc"] === "on" });
|
|
@@ -1369,7 +1420,7 @@ async function runDriver(argv, module, deps = realRunDeps()) {
|
|
|
1369
1420
|
let flushOff = [];
|
|
1370
1421
|
const iterations = [];
|
|
1371
1422
|
let terminating = false;
|
|
1372
|
-
const logFile =
|
|
1423
|
+
const logFile = join2(out, "driver-log.json");
|
|
1373
1424
|
const persist = () => {
|
|
1374
1425
|
const last = t.stepLog.at(-1);
|
|
1375
1426
|
const body = {
|
|
@@ -1410,7 +1461,7 @@ async function runDriver(argv, module, deps = realRunDeps()) {
|
|
|
1410
1461
|
}
|
|
1411
1462
|
};
|
|
1412
1463
|
let dashboard = null;
|
|
1413
|
-
const writeBuild = (failure) => writeAtomic(
|
|
1464
|
+
const writeBuild = (failure) => writeAtomic(join2(out, `browserstack.${String(a.platform)}.json`), `${JSON.stringify({
|
|
1414
1465
|
driver: "appium",
|
|
1415
1466
|
status: failure === null ? "passed" : "failed",
|
|
1416
1467
|
buildId,
|
|
@@ -1578,7 +1629,7 @@ async function runIterations(t, module, ctx, iterations, progress) {
|
|
|
1578
1629
|
|
|
1579
1630
|
// packages/appium-driver/src/load.ts
|
|
1580
1631
|
async function loadJourneyModule(path, cwd = process.cwd()) {
|
|
1581
|
-
const file = isAbsolute(path) ? path :
|
|
1632
|
+
const file = isAbsolute(path) ? path : resolve2(cwd, path);
|
|
1582
1633
|
const mod = await import(pathToFileURL(file).href);
|
|
1583
1634
|
const m = mod.default ?? mod;
|
|
1584
1635
|
if (!m.app?.bundleId || !m.app.scheme)
|