@keepsnappy/agent 0.1.0-alpha.14 → 0.1.0-alpha.15

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.
@@ -348,6 +348,10 @@ class Driver {
348
348
  }
349
349
  return value;
350
350
  }
351
+ async attribute(el, name) {
352
+ const value = await this.cmd("GET", `/element/${el}/attribute/${name}`);
353
+ return value === this.platform.absentAttributeText ? null : value;
354
+ }
351
355
  async start(capabilities) {
352
356
  const value = await this.cmd("POST", "/session", { capabilities: { alwaysMatch: capabilities } });
353
357
  this.sessionId = value.sessionId;
@@ -427,7 +431,7 @@ class Driver {
427
431
  break;
428
432
  let resourceId;
429
433
  try {
430
- resourceId = String(await this.cmd("GET", `/element/${el}/attribute/resource-id`));
434
+ resourceId = String(await this.attribute(el, "resource-id") ?? "");
431
435
  } catch (e) {
432
436
  if (e instanceof WdError && e.wdError === "stale element reference")
433
437
  continue;
@@ -530,6 +534,7 @@ var IOS = {
530
534
  "appium:newCommandTimeout": NEW_COMMAND_TIMEOUT_S,
531
535
  "appium:waitForIdleTimeout": NO_WAIT_MS
532
536
  }),
537
+ absentAttributeText: null,
533
538
  settings: { animationCoolOffTimeout: NO_WAIT_MS, waitForIdleTimeout: NO_WAIT_MS, shouldUseCompactResponses: true },
534
539
  locators: (sel) => [locator(sel)],
535
540
  keyboardShown: async (driver) => Boolean(await driver.findShown(id("Return"))),
@@ -548,7 +553,7 @@ async function iosCommand(driver, link, cmd, n, state, waitMs) {
548
553
  for (;; ) {
549
554
  const el = await driver.find("-ios predicate string", want);
550
555
  if (el)
551
- return driver.cmd("GET", `/element/${el}/attribute/label`).then(String).catch(() => {
556
+ return driver.attribute(el, "label").then(String).catch(() => {
552
557
  driver.log.push({ ...driver.context, cmd: `${cmd}-ack-label`, t: driver.now(), ms: 0, status: 1 });
553
558
  return `${cmd}:${n} ok`;
554
559
  });
@@ -568,6 +573,7 @@ var ANDROID = {
568
573
  "appium:autoGrantPermissions": true,
569
574
  "appium:newCommandTimeout": NEW_COMMAND_TIMEOUT_S
570
575
  }),
576
+ absentAttributeText: "null",
571
577
  settings: { waitForIdleTimeout: NO_WAIT_MS, actionAcknowledgmentTimeout: NO_WAIT_MS, waitForSelectorTimeout: NO_WAIT_MS, enableMultiWindows: true, ignoreUnimportantViews: true },
572
578
  locators: androidLocators,
573
579
  appArg: (appId) => ({ appId }),
@@ -778,6 +784,7 @@ var FLUSH_MARGIN_MS = 30000;
778
784
  var FLUSH_WAIT_MS = SDK_FLUSH_WORST_CASE_MS + FLUSH_MARGIN_MS;
779
785
  var FLUSH_POLL_MS = 500;
780
786
  var FLUSH_RESEND_MS = 90000;
787
+ var ACTIVATION_SETTLE_MS = 2000;
781
788
  var FLUSH_NOT_ACKNOWLEDGED = "not acknowledged (re-sent once)";
782
789
  var FLUSH_UNREADABLE = "not acknowledged (unreadable answer)";
783
790
  var FLUSH_STATUSES = new Set(["ok", "error", "off"]);
@@ -1182,7 +1189,9 @@ class Session {
1182
1189
  this.pendingReady = null;
1183
1190
  await this.relaunch();
1184
1191
  await this.waitVisible(home, 30000);
1185
- return this.openLink(activationLink(this.scheme, token, args));
1192
+ const alert = await this.openLink(activationLink(this.scheme, token, args));
1193
+ await this.driver.sleep(ACTIVATION_SETTLE_MS);
1194
+ return alert;
1186
1195
  }
1187
1196
  lastFlush = null;
1188
1197
  flushSeq = 0;
@@ -1226,8 +1235,8 @@ class Session {
1226
1235
  if (!el && flush.fallback && poll % FLUSH_FALLBACK_EVERY === 0)
1227
1236
  el = await this.findFallback(flush.fallback);
1228
1237
  if (el) {
1229
- const label = await this.driver.cmd("GET", `/element/${el}/attribute/${flush.statusAttribute}`).catch(() => null);
1230
- const raw = await this.driver.cmd("GET", `/element/${el}/attribute/${flush.detailAttribute}`).then((value) => ({ value }), () => null);
1238
+ const label = await this.driver.attribute(el, flush.statusAttribute).catch(() => null);
1239
+ const raw = await this.driver.attribute(el, flush.detailAttribute).then((value) => ({ value }), () => null);
1231
1240
  const detail = typeof raw?.value === "string" ? raw.value : null;
1232
1241
  const echoed = detail === null ? null : ACK_ECHO.exec(detail);
1233
1242
  if (raw === null) {
@@ -5063,6 +5063,7 @@ var STATE_WORDS = {
5063
5063
  "no-change": "No change",
5064
5064
  "cant-tell": "Can’t tell"
5065
5065
  };
5066
+ var INFO_WORDS = { heat: "Phone heated up" };
5066
5067
  var NOT_BLOCKING = "not blocking";
5067
5068
  // packages/present/dist/src/values.js
5068
5069
  var NO_VALUE = "–";
@@ -5166,6 +5167,13 @@ function summarize(input) {
5166
5167
  // apps/agent/src/remote.ts
5167
5168
  var isBlocking = (s) => !!s?.verdicts && (s.verdicts.blocking ?? s.verdicts.state === "worse");
5168
5169
  var midLine = (label, metric) => !metric.startsWith("component:") && /^[A-Z][a-z]*(\s|$)/.test(label) ? `${label[0].toLowerCase()}${label.slice(1)}` : label;
5170
+ function heatNote(sc) {
5171
+ const n = sc.heated_runs ?? 0;
5172
+ if (!n)
5173
+ return null;
5174
+ const phone = sc.device ? `${sc.device.model}${sc.device.os ? ` · ${sc.device.os}` : ""}` : "unknown phone";
5175
+ return `${INFO_WORDS.heat} in ${n} of ${sc.runs.valid + sc.runs.invalid} runs · ${sc.scenario ?? "journey"} · ${phone}`;
5176
+ }
5169
5177
  function movedItems(v, sc) {
5170
5178
  const flat = v.items.filter((i) => i.scenario === sc.scenario && (!sc.device || !i.device_id || i.device_id === sc.device.id));
5171
5179
  const h = sc.headline;
@@ -5263,11 +5271,16 @@ function formatRunSet(s, top = 5) {
5263
5271
  lines.push(` ${FEW_RUNS}`);
5264
5272
  if (sc.role !== "adhoc" && !noCounts(movedCounts(sc)))
5265
5273
  lines.push(` ${countsLine(movedCounts(sc))}`);
5266
- const moved = movedItems(v, sc).sort((a, b) => Number(b.state === "worse") - Number(a.state === "worse") || Number(a.grade === "warn") - Number(b.grade === "warn")).slice(0, top);
5274
+ if (heatNote(sc))
5275
+ lines.push(` ${heatNote(sc)}`);
5276
+ const allMoved = movedItems(v, sc).sort((a, b) => Number(b.state === "worse") - Number(a.state === "worse") || Number(a.grade === "warn") - Number(b.grade === "warn"));
5277
+ const moved = allMoved.slice(0, top);
5267
5278
  for (const i of moved) {
5268
5279
  const what = i.label_text ? `${i.step && i.step !== "*" ? `${placeLabel(i.step)}: ` : ""}${i.label_text}` : i.step ? `${placeLabel(i.step)}: ${fieldWord(i.metric)}` : plainMetric(i.metric);
5269
5280
  lines.push(` ${stateWord(i.state === "worse" && i.grade === "warn" ? "warn" : i.state).padEnd(WORD_WIDTH)} ${what}${i.change_text ? `: ${i.change_text}` : change(i.metric, i.baseline, i.candidate)}`);
5270
5281
  }
5282
+ if (allMoved.length > moved.length)
5283
+ lines.push(` ${allMoved.length - moved.length} more`);
5271
5284
  if (sc.dashboard_url)
5272
5285
  lines.push(` ${sc.dashboard_url}`);
5273
5286
  }
@@ -5288,6 +5301,7 @@ Runs uploaded for \`${fallback.branch}\`. The result appears on the dashboard: [
5288
5301
  `;
5289
5302
  }
5290
5303
  const v = s.verdicts;
5304
+ const heated = v.scenarios.flatMap((sc) => heatNote(sc) ?? []);
5291
5305
  const rows = v.scenarios.map((sc) => {
5292
5306
  const c = movedCounts(sc);
5293
5307
  return `| ${sc.scenario ?? "-"} | ${sc.device ? sc.device.model : "-"} | ${word(sc.state, sc.word, sc.headline)}${sc.low_data ? ` (${FEW_RUNS})` : ""} | ${noCounts(c) ? "–" : `${c.worse} / ${c.warnings} / ${c.better} / ${c.noChange} / ${c.inconclusive}`} | ${sc.dashboard_url ? `[open](${sc.dashboard_url})` : "-"} |`;
@@ -5301,6 +5315,7 @@ Runs uploaded for \`${fallback.branch}\`. The result appears on the dashboard: [
5301
5315
  `| Journey | Device | Result | ${STATE_WORDS.worse} / ${STATE_WORDS.warning} / ${STATE_WORDS.better} / ${STATE_WORDS["no-change"]} / ${STATE_WORDS["cant-tell"]} | Details |`,
5302
5316
  "|---|---|---|---|---|",
5303
5317
  ...rows,
5318
+ ...heated.length ? ["", ...heated.map((note) => `${note}.`)] : [],
5304
5319
  "",
5305
5320
  `[Open the dashboard](${s.dashboard_url})`,
5306
5321
  ""
@@ -9548,6 +9563,7 @@ var ANDROID = {
9548
9563
  "appium:autoGrantPermissions": true,
9549
9564
  "appium:newCommandTimeout": NEW_COMMAND_TIMEOUT_S
9550
9565
  }),
9566
+ absentAttributeText: "null",
9551
9567
  settings: { waitForIdleTimeout: NO_WAIT_MS, actionAcknowledgmentTimeout: NO_WAIT_MS, waitForSelectorTimeout: NO_WAIT_MS, enableMultiWindows: true, ignoreUnimportantViews: true },
9552
9568
  locators: androidLocators,
9553
9569
  appArg: (appId) => ({ appId }),
@@ -9623,7 +9639,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
9623
9639
  import { dirname as dirname2, join as join11, relative as relative2, resolve as resolve4 } from "node:path";
9624
9640
  import { fileURLToPath } from "node:url";
9625
9641
  function driverVersion() {
9626
- const built = "0.1.0-alpha.14";
9642
+ const built = "0.1.0-alpha.15";
9627
9643
  if (built)
9628
9644
  return built;
9629
9645
  let dir = dirname2(fileURLToPath(import.meta.url));
@@ -9685,8 +9701,6 @@ function metricWord(item) {
9685
9701
  return measureDef(item.metric)?.short ?? item.metric.replace(/_/g, " ");
9686
9702
  }
9687
9703
  function change2(item) {
9688
- if (item.metric === HEAT_METRIC && item.baseline !== null && item.candidate !== null)
9689
- return `+${Math.round((item.candidate - item.baseline) * 100)} pp`;
9690
9704
  if (item.change_text)
9691
9705
  return item.change_text.replace(/ \(.*\)$/, "");
9692
9706
  const { baseline: b, candidate: c } = item;
@@ -9699,7 +9713,7 @@ function change2(item) {
9699
9713
  }
9700
9714
  var size = (i) => i.baseline === null || i.candidate === null ? 0 : i.baseline === 0 ? i.candidate === 0 ? 0 : Number.POSITIVE_INFINITY : Math.abs(Math.log(Math.max(i.candidate, 0.000000001) / i.baseline));
9701
9715
  var bySize = (a, b) => size(a) === size(b) ? 0 : size(a) > size(b) ? -1 : 1;
9702
- var worseOrder = (a, b) => Number(a.grade === "warn") - Number(b.grade === "warn") || Number(b.metric === HEAT_METRIC) - Number(a.metric === HEAT_METRIC) || bySize(a, b);
9716
+ var worseOrder = (a, b) => Number(a.grade === "warn") - Number(b.grade === "warn") || bySize(a, b);
9703
9717
  var isWorse = (i) => i.state === "worse";
9704
9718
  var isBetter = (i) => i.state === "better";
9705
9719
  var differs = (sc) => sc?.headline?.measured_differently ?? [];
@@ -9765,13 +9779,20 @@ function slackMessage(s, ctx) {
9765
9779
  const worse = items.filter(isWorse).sort(worseOrder);
9766
9780
  const better = items.filter(isBetter).sort(bySize);
9767
9781
  const commit = commitLine(ctx);
9782
+ const heated = v.scenarios.filter((sc) => heatNote(sc));
9783
+ const heat = heated.map((sc) => context(heatNote(sc)));
9768
9784
  const errorRows = worse.filter((i) => i.grade !== "warn");
9769
9785
  const warningRows = worse.filter((i) => i.grade === "warn");
9770
9786
  const errors = placeCount(errorRows);
9771
9787
  const warns = placeCount(worse) - errors;
9772
9788
  if (isPr) {
9773
- if (!worse.length)
9774
- return null;
9789
+ if (!worse.length) {
9790
+ if (!heat.length)
9791
+ return null;
9792
+ const head = `⚪ ${ctx.branch}: ${INFO_WORDS.heat}`;
9793
+ const blocks = [section(`*${head}*`), ...heat, ...commit ? [context(commit)] : [], section(`\uD83D\uDC49 <${heated[0].dashboard_url ?? s.dashboard_url}|View the comparison>`)];
9794
+ return { text: head, attachments: [{ color: COLOR.none, blocks }] };
9795
+ }
9775
9796
  const red = worse.some((i) => isRed(i, scenarioOf(i)));
9776
9797
  const graded = worse.some((i) => i.grade);
9777
9798
  const notBlocking = errors > 0 && !red;
@@ -9797,17 +9818,19 @@ _Measured differently (${differs(sc).join(", ")})${scenarioBlocks(sc) ? "" : `
9797
9818
  ${prTable(rows, sc)}`));
9798
9819
  }
9799
9820
  const more = worse.length - shown;
9821
+ blocks.push(...heat);
9800
9822
  if (commit)
9801
9823
  blocks.push(context(commit));
9802
9824
  blocks.push(section(`\uD83D\uDC49 <${groups.keys().next().value?.dashboard_url ?? s.dashboard_url}|View the comparison>${more > 0 ? ` · ${more} more` : ""}`));
9803
9825
  return { text: head, attachments: [{ color: red ? COLOR.error : COLOR.warn, blocks }] };
9804
9826
  }
9805
- if (!worse.length && !better.length && !ctx.always)
9827
+ const improved = better.length > 0 || v.state === "better";
9828
+ if (!worse.length && !improved && !ctx.always && !heat.length)
9806
9829
  return null;
9807
9830
  const parts = [errors ? `${errors} ${STATE_WORDS.worse.toLowerCase()}` : "", warns ? `${warns} ${STATE_WORDS.warning.toLowerCase()}` : "", better.length ? `${placeCount(better)} ${STATE_WORDS.better.toLowerCase()}` : ""].filter(Boolean);
9808
- const quiet = (v.state === null || v.state === "inconclusive") && v.word ? v.word : STATE_WORDS["no-change"].toLowerCase();
9809
- const status = parts.length ? `${ctx.branch}: ${parts.join(", ")}` : quiet.startsWith("Main: ") && ctx.branch === "main" ? quiet : `${ctx.branch}: ${quiet}`;
9810
- const head = `${worse.length ? worse.some((i) => isRed(i, scenarioOf(i))) ? "\uD83D\uDD34" : "\uD83D\uDFE0" : better.length ? "\uD83D\uDFE2" : "⚪"} ${status}`;
9831
+ const quiet = improved ? STATE_WORDS.better.toLowerCase() : (v.state === null || v.state === "inconclusive") && v.word ? v.word : STATE_WORDS["no-change"].toLowerCase();
9832
+ const status = parts.length ? `${ctx.branch}: ${parts.join(", ")}` : !improved && heat.length && !ctx.always ? `${ctx.branch}: ${INFO_WORDS.heat}` : quiet.startsWith("Main: ") && ctx.branch === "main" ? quiet : `${ctx.branch}: ${quiet}`;
9833
+ const head = `${worse.length ? worse.some((i) => isRed(i, scenarioOf(i))) ? "\uD83D\uDD34" : "\uD83D\uDFE0" : improved ? "\uD83D\uDFE2" : "⚪"} ${status}`;
9811
9834
  const journeys = new Set(v.scenarios.map((sc) => `${sc.scenario}|${sc.device?.id}`)).size;
9812
9835
  const blocks = [section(`*${head}*
9813
9836
  ${journeys} journey${journeys === 1 ? "" : "s"} checked`)];
@@ -9823,10 +9846,11 @@ ${mainCard(i)}`));
9823
9846
  }
9824
9847
  }
9825
9848
  const more = worse.length + better.length - shown;
9849
+ blocks.push(...heat);
9826
9850
  if (commit)
9827
9851
  blocks.push(context(commit));
9828
9852
  blocks.push(section(`\uD83D\uDC49 <${s.dashboard_url}|Open KeepSnappy>${more > 0 ? ` · ${more} more changes` : ""}`));
9829
- const color = worse.length ? worse.some((i) => isRed(i, scenarioOf(i))) ? COLOR.error : COLOR.warn : better.length ? COLOR.better : COLOR.none;
9853
+ const color = worse.length ? worse.some((i) => isRed(i, scenarioOf(i))) ? COLOR.error : COLOR.warn : improved ? COLOR.better : COLOR.none;
9830
9854
  return { text: head, attachments: [{ color, blocks }] };
9831
9855
  }
9832
9856
  async function postToSlack(url, payload, fetchImpl = fetch) {
@@ -11034,10 +11058,10 @@ function formatFinding(f, opts = {}) {
11034
11058
  line("Layer", `${f.layer.id} · ${f.layer.name} (${f.certificate ? "certified" : "uncertified"})${f.limits.length ? `; limits: ${f.limits.join("; ")}` : ""}`),
11035
11059
  line("Id", f.id)
11036
11060
  ];
11037
- if (f.why.components.length > 1) {
11061
+ if (f.why.components.length) {
11038
11062
  out.push(line("Components", ""));
11039
11063
  for (const c of f.why.components) {
11040
- out.push(` ${c.component}: ${c.self_ms_before} → ${c.self_ms_after} ms own time, renders ${c.renders_before} → ${c.renders_after}${c.cause ? `; ${c.cause}` : ""}${c.flagged ? "" : " (not certain)"}`);
11064
+ out.push(` ${c.component}: ${c.self_ms_before ?? "–"} → ${c.self_ms_after} ms own time, renders ${c.renders_before ?? "–"} → ${c.renders_after ?? "–"}${c.cause ? `; ${c.cause}` : ""}${c.flagged ? "" : " (not certain)"}`);
11041
11065
  }
11042
11066
  }
11043
11067
  if (opts.prompt)
@@ -11075,16 +11099,16 @@ function measuredLine(l) {
11075
11099
  function countsLine2(l) {
11076
11100
  const c = l.counts;
11077
11101
  const parts = [
11078
- c.worse ? `${c.worse} worse${c.not_blocking ? ` (${c.not_blocking} not blocking)` : ""}` : "",
11079
- c.warn ? plural(c.warn, "warning") : "",
11080
- c.better ? `${c.better} better` : ""
11102
+ c.worse ? `${c.worse} ${STATE_WORDS.worse.toLowerCase()}${c.not_blocking ? ` (${c.not_blocking} not blocking)` : ""}` : "",
11103
+ c.warn ? `${c.warn} ${STATE_WORDS.warning.toLowerCase()}` : "",
11104
+ c.better ? `${c.better} ${STATE_WORDS.better.toLowerCase()}` : ""
11081
11105
  ].filter(Boolean);
11082
11106
  const state = l.filters.state;
11083
11107
  const unjudged = l.measured.lanes.filter((x) => !x.judged);
11084
- const head = parts.length ? `${parts.join(", ")}${state === "all" ? "" : `, ${STATE_WORDS2[state] ?? state}`}` : state !== "present" && state !== "all" ? `No finding ${STATE_WORDS2[state] ?? state}` : unjudged.length === l.measured.lanes.length ? `No finding, but nothing was compared yet (${[...new Set(unjudged.map((x) => x.word))].join(", ")}): measure again before calling it clean` : "Nothing worse, no warning, nothing better";
11108
+ const head = parts.length ? `${parts.join(", ")}${state === "all" ? "" : `, ${STATE_WORDS2[state] ?? state}`}` : state !== "present" && state !== "all" ? `No finding ${STATE_WORDS2[state] ?? state}` : unjudged.length === l.measured.lanes.length ? `No finding, but nothing was compared yet (${[...new Set(unjudged.map((x) => x.word))].join(", ")}): measure again before calling it clean` : `Nothing ${STATE_WORDS.worse.toLowerCase()}, nothing ${STATE_WORDS.warning.toLowerCase()}, nothing ${STATE_WORDS.better.toLowerCase()}`;
11085
11109
  const heated = c.heat_runs ?? 0;
11086
11110
  const more = [
11087
- heated ? `${plural(heated, "run")} heated up the phone (run health, layer 3)` : "",
11111
+ heated ? `${INFO_WORDS.heat.toLowerCase()} in ${plural(heated, "run")} (run health, layer 3)` : "",
11088
11112
  state === "present" && l.states.not_judged ? `${l.states.not_judged} more not judged in the newest run set (state not-judged)` : ""
11089
11113
  ].filter(Boolean);
11090
11114
  const line = `${head}${more.length ? `; ${more.join("; ")}` : ""}.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keepsnappy/agent",
3
- "version": "0.1.0-alpha.14",
3
+ "version": "0.1.0-alpha.15",
4
4
  "description": "KeepSnappy CLI: runs your app journeys, measures from inside the app and reports More work / Less work / No change per step.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",