npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@keepsnappy/appium-driver

v0.1.0-alpha.25

Published

KeepSnappy Appium driver: write a journey module, run it on BrowserStack with `keepsnappy farm run --driver appium --journey-module`.

Readme

@keepsnappy/appium-driver

KeepSnappy's built-in Appium driver for keepsnappy farm run --driver appium --journey-module <file> (BrowserStack App Automate, iOS and Android; --platform ios|android, default ios). A thin step vocabulary over W3C WebDriver: you write the journey, the driver owns the session, run tokens, step markers, the readiness wait (expectNext) after a touch and the result files. Design: docs/plans/archive/54b-appium-driver.md.

A journey module default-exports defineJourney({ app, home, options?, setup?, beforeLaunch?, journeys }); see the plan for the full shape. setup runs once per session; beforeLaunch runs once per iteration, after the activation link and before the launch the iteration is measured in: put state outside the app that every run must start in there (a mock server's world reset), never in the journey, which starts in the measured process. It may not run a step, a navigation pass or a GC; its error fails the session. A journey needs no import from this package: selectors are plain objects ({ id }, { text }, { ...sel, childOf }, or a bare string) and everything else is on the t it is given (t.acceptAlert, t.openLink, ...). defineJourney is only an identity helper for editor types. The CLI runs its own bundled copy, so do not instanceof driver errors (WdError) from your own copy; test e.wdError.

Run directly (what farm run does): node dist/src/cli.js --journey-module j.mjs --app bs://… --tokens-js tokens.js --journeys a,b --repeat 3 --out DIR [--local-identifier ID], with BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY. Exit 0, or 1 when the session or an iteration failed (the result files are written either way).

Step checks and Android

t.waitAll([{ shown: sel }, { gone: sel }, { keyboard: 'up' }, ...]) waits until all conditions hold in one poll (the poll asks them one lookup after another). On Android the ids are asked in one UiAutomator2 lookup (a walk of the whole tree costs the same for one id and for ten); elsewhere each condition is waited for in turn. The keyboard condition is answered by the platform (Android mobile: isKeyboardShown, iOS the Return key), so a journey needs no platform branch. t.swipeFromUntilVisible(from, direction, target, times) swipes until a target shows. t.flingTimes(list, n, 'end' | 'top') flings a list exactly n times (the list gesture, FLING) whatever it holds, so a judged step does the same touches in every run (REQ-FARM-33); t.flingUntil(list, target, toward, { blind }) flings until a target shows, a data-dependent count, so not for a judged step. t.stableCentres(sels) returns elements' centres once their frames stop moving and t.tapAt(point) taps there with the same touch as tap. A frame read of an element the app re-mounted (a stale element reference) looks it up again, at most 3 times per read: the touches stay the same, and the window of the touch before is longer by one lookup.

Android specifics (UiAutomator2, measured on a Galaxy S23): a lookup that finds nothing walks the whole tree (about 1.2 s), one that finds a shown element takes 0.1 to 0.2 s, so findShown asks for one element first. ignoreUnimportantViews is on (about 30 % faster lookups).

Inverted bounds: when the keyboard opens, TapIn translates the chat up while Android still cuts the chat nodes by the container's untranslated rect. The chat's scroll container and its nodes come back with top below bottom and displayed=false, and are left out of lookups. While the keyboard is up (and at most every 3 s) waitAll reads the page source with allowInvisibleElements and counts such a node as shown when it sits in an inverted scroll container, its real bottom edge is on screen and below the edge the clipped-away nodes pile up on (at least two clipped messages are needed to find the edge). See src/snapshot.ts.

How to write journeys that run fast on Android, with measured costs: docs/writing-fast-journeys.md. How to write or convert a journey, for people and coding agents: the skill docs/skills/keepsnappy-journey-authoring/SKILL.md (repo root).

GC before the action (--action-gc on|off, default off)

With --action-gc on the driver asks the app for a full garbage collection before every declared step, after the step's readiness wait and before its start marker, and once more before the run's flush. Design: docs/plans/archive/gc-before-action.md; device checks: docs/plans/archive/gc-before-action-measurement.md.

  • Each command has a number n, unique in the session. Android sends it as a broadcast (mobile: broadcast, action <bundleId>.keepsnappy.COMMAND, extras cmd=gc, n) and reads the answer gc:<n> ... from the broadcast's result. iOS opens <scheme>devtools/keepsnappy?cmd=gc&n=<n> and waits up to 10 s for the element keepsnappy.ack whose label starts with gc:<n> .
  • The step's start marker carries gc_n, so the backend (derivation 18) pairs the step with its GC record without mapping clocks.
  • No answer turns the command off for the rest of the session (actionGc: unsupported in driver-log.json); the test still passes. Safari link mode sends none (safari).
  • The GC command ends the app's open windows: runs with and without it are a different instrument (actionGc aspect), never compared as an app change.

GC at the run end (--end-gc on|off, default on)

One GC command at the end of every run, after the journey's last step and its readiness wait, before the markers and the flush, also when --action-gc is off. Its record gives the end reading after a GC (derivation 19: mem.end_after_gc_mb, mem.retained_growth_mb, mem.js_heap_end_mb; setup fact sdk.end_gc). End the journey on its home screen (docs/writing-fast-journeys.md), so the reading holds what the journey kept, not an open heavy screen; sdk.journey_ends_home = 0 flags a run that did not.

  • Same command and fallbacks as above; an app that does not answer turns it off for the rest of the session.
  • The command ends the last open window (end reason gc-command) instead of the flush: that one window per run is shorter than on a driver without it. Driver 0.2.0 carries both commands, so runs before and after it are a different instrument already (driverVersion aspect).
  • On by default (REQ-MEAS-35, REQ-FARM-28; driver 0.3.0); --end-gc off or endGc: false turns it off. It changes the memory readings only (it runs after the last window): a main without it still judges a PR's render, commit and component measures; only the memory verdict needs end-GC runs on both sides (endGc setup aspect, memory measures only). Older SDKs do not answer and send no line.
  • Right after the GC's answer the driver sends memread (same n) and waits for its answer before it sends the markers and the flush link, which background the app. The SDK takes one memory sample at once (endgc-read:<n>); memory kept is derived from it (derivation 23). No answer: logged as memread status 1, the run goes on, and later runs of the session do not ask.

The default run configuration (links.ts)

RUN_DEFAULTS and the link builders are the one source of the activation, flush and GC links (REQ-FARM-28): profile=1, diag=1 (the React export), ttl 3600, no cpuprof, and the end GC on (END_GC_DEFAULT). A journey module's sdkArgs (k=v&...) are set over the defaults key by key, so sdkArgs: 'profile=1' still gets diag=1; diag=0 turns the export off explicitly. keepsnappy activation-link | flush-link | gc-link print the same links for runners that are not this driver, and keepsnappy farm open writes the settings into tokens.js (output.keepsnappyLinkArgs) for Maestro flows. Maestro runs send no GC for now. A guard test in apps/agent fails on a hand-written activation link anywhere else in the repo.

driver-log.json while the run goes

<out>/driver-log.json is rewritten atomically at every top-level step and iteration boundary, so a killed run still leaves a valid log. status is running, done, terminated or error; lastStep names the last step started and steps times every step (ms: null while one runs). On SIGTERM or SIGINT (the farm's step timeout) the driver flushes the log as terminated with the signal, captures the failure screen and source (at most 20 s), ends the session as failed (at most 10 s) and exits 143 or 130, within 45 s in all; a second signal exits at once. keepsnappy farm run counts a driver that exits non-zero as infrastructure.

List and gesture helpers

Import standalone helpers and pass the journey's Session (t). Selectors, page boundaries and step names come from your journey. Selectors match whole values, so anchor a row label (^Result 1$) when another row starts with the same text:

import { childOf, id, text, scrollCycles } from '@keepsnappy/appium-driver';

await scrollCycles(t, {
  list: id('results.list'),
  first: childOf(text('^Result 1$'), 'results.list'),
  any: text('Result .*'),
  pages: [text('^Result 21$'), text('^Result 41$')],
  pageNumbers: [21, 41],
  beforePages: [text('^Result 20$'), text('^Result 40$')],
  lastPage: text('Result (41|42|43)'),
  field: 'text',
  scoped: true,
  stepName: (toward, cycle) => `results.scroll-to-${toward}-${cycle}`,
});

scrollList(t, { list, row, field, screen, above }, toward, { cache, hold }) is the one way the driver scrolls a list; every helper below sends its gesture through it. With mustMove (a to-top pass, the first fling of an end pass) the gesture is checked: one the device did not take (the held row's frame unchanged, both platforms) is confirmed by the row ids inside the list's frame, sent once more and counted in the driver log's defects; a second one throws GestureNotTaken. Every list gesture is one touch, FLING, on both platforms: from 85 % of the list frame to 25 % (toward the end) or from 30 % to 90 % (toward the top), i.e. 60 % of the list, at least 250 dp (Android) or pt (iOS), held for 150 ms. Android sends it as a native UiAutomator mobile: swipeGesture over that span at the speed that makes it last 150 ms (density cached per driver; a swipe answers when the finger lifts, a fling waits about 5 s for a scroll event), iOS as a 150 ms drag. A touch that would reach into the bottom system-gesture zone or past the window's top moves whole; its start stays at least 10 % of the list inside the list's frame, and a touch that still does not fit is cut and logs clamped: true and travel. An app takes a touch as a pan 19–60 ms after it lands, so a shorter touch can move nothing. longScroll is that gesture on a given rectangle. In a measured step flingUntilShown stops when any part of the target shows: three gestures maximum, five toward the top on iOS. scrollUntilShown exposes the same capped look/move loop for custom checks. waitCoasted waits for two matching row frames 300 ms apart (up to 3000 ms); holdBeforeScroll captures a row before an iOS gesture so coast checks can reuse its handle.

scrollToEnd performs three long scrolls and no other: never a catch-up gesture. Each page marker starts a 3000 ms load wait after coast stops (6000 ms Android, 10000 ms iOS); scrolls without a page marker pause 500 ms. A page that has not landed fails the pass, or with deferPages is logged (landed) and left to navigation. scrollCycles repeats three end/top pairs, each step a fixed count of the measured gesture (3 toward the end, 2 toward the top), waiting for page markers only in cycle one; after each step a navigation pass finishes the way and the end check (lastPage loaded) and top check run.

Navigation passes (navigate, Session.navigation): getting somewhere — to the top, to the loaded end, a target into view — is not a measured gesture. A pass is wrapped in nav start/end markers, outside any measured step, and its list gestures are the navigation stroke (NAV_STROKE: a 100 ms slow move the app takes as a pan, then a 45 ms flick, one W3C actions call on both platforms). It ends when the target is found by lookup and the list's first and last rows are unchanged 300 ms apart, never on a frame test; two strokes that leave them as they were throw NavigationStuck. flingUntilShown, scrollToTarget, Session.flingUntil and Session.scrollUntil are navigation passes outside a step and send the measured gesture inside one. pageNumbers preserves numeric page labels in command summaries; otherwise summaries use selector descriptions. Page sizes and seed data stay in the journey's page selectors. Options expose the counts and timeouts; their defaults are exported constants. frameTimeoutMs (20000) bounds the wait for the list frame to stop moving, checkTimeoutMs (20000) bounds a step check (passChecked, endChecked; in scrollCycles the top check reads fling.checkTimeoutMs), and lookWindowMs (1000) is the window in which a look after a gesture is retried. The lookMs field in the log stays the cumulative time spent looking.

passChecked and endChecked create step checks; share a ListCache with flingUntilShown/scrollToEnd for each visit to a list. passChecked permits one repair using only the original pass's remaining budget. scoped: false searches globally when row selectors are unique. field: 'text' | 'description' limits Android row lookups to the attribute the app uses. rowAlternatives(from, to, format) builds page-marker alternations from journey data; rowAtOrBelow checks whether a row has moved under a boundary. t.frameOf and t.settledFrame expose the existing stale-safe frame readers for custom mechanics.

androidScreenScroll is the list gesture on an inverted list's frame. scrollFromDrawnRow uses that Android path or starts the iOS touch on a drawn row, avoiding masked padding. Toward the top on iOS it starts on the lowest drawn row that ends above above (e.g. a composer), chosen by frame: rows in a list's faded top zone pass touches through. When the touch would reach into the home-indicator zone it starts higher by the same amount (on a drawn row above above, inside the list) so its travel stays; with no such point it keeps its start, ends at the limit and logs clamped: true and travel. Pass missingMessage to keep your own error text when no drawn row is found. scrollToTarget waits for an initial target, optionally taps a reveal indicator, then uses a caller-supplied move with coast waits and a three-gesture cap. closeSwipe, closeSwipeAt (window percentages) and closeSwipeFrom (element centre) use native Android sheet drags or 400 ms iOS drags. tapAllAt sends points in one W3C sequence with a 100 ms hold and 45 ms gap per point.

Helpers log the underlying WebDriver commands; list loops also record gesture counts, checks, durations and failure status. Re-scroll checks emit a re-scrolled event. Keep caller-specific log names and error messages by passing cmd, field and missing to the custom loop, or cmd/missing to scrollToTarget.