Repository navigation
Journey runner: a phone device per journey, offline/online steps and a GPS fix per step (patch offered) #521
Description
Files: project-tests/e2e/journey-runner.js, project-tests/e2e/journey-runner.selftest.js, examples/validate-journeys.js (ACTIONS, line 32), skills/journey-examples.md · Toolkit: eec646e · Found: 20 Oct 2026 · Patch: offered (written and self-tested in a project copy of the runner)
Observed
The runner opens every journey in one 1440x900 desktop window. journey-runner.js calls H.launchBrowser() with no
arguments, and config.js sets viewport: { width: 1440, height: 900 }. Three things follow for a Mendix PWA:
- No phone profile. Mendix serves the Responsive profile to a desktop window, not Phone or PhoneOffline. A phone
journey fails at step 1 for a test-setup reason, not because the feature is broken.helpers.launchBrowserAt(..., { mobile: true })
has a real Playwright device profile, but the runner never calls it. - No way to go offline. No file contains
setOffline. On one project, 4 journeys put a placeholderwaitwhere the
browser must go offline or come back online:
{ "do": "wait", "label": "SET THE BROWSER OFFLINE HERE (context.setOffline(true))" }. A run of those journeys never
actually leaves the network, so the "offline" steps measure the online app. - No GPS position. A check-in nanoflow that reads the device position (NanoflowCommons
GetCurrentLocationMinimumAccuracy) gets "permission denied". The step then fails because the test set no position.
On that project, 10 steps across 9 journey files name a position in_geolocation, which the runner does not read.
20 of that project's 57 journey files declare "device": { "name": "iPhone 13", "viewport": { "width": 390, "height": 844 } },
and every runner result for them had to be ruled INVALID.
What the offered patch does
549 lines added and 7 changed across the runner and its selftest. Desktop journeys behave exactly as before: a journey
with no device or viewport still goes through H.launchBrowser() with no arguments.
| Feature | Journey field | What the runner does |
|---|---|---|
| Phone device | top level "device": "iPhone 13" or { "name", "viewport": { width, height } } |
Opens a context with the Playwright profile's user agent, isMobile, hasTouch and deviceScaleFactor. The size comes from device.viewport, then the profile's screen (390x844 for iPhone 13, not its 390x664 viewport). Refuses an unknown name and lists the nearest known ones; it never swaps in a neighbouring profile. |
| Phone by size | top level "viewport": { width, height, "isMobile": true } |
Phone at that size, with the iPhone 13 user agent. Without isMobile: true it opens a desktop window of that size. |
| Run override | JOURNEY_DEVICE=<name> or desktop |
Applies one device to every journey in the run. |
| One device per run | (preflight) | One run is one context and one sign-in. A run whose journeys need different devices exits 2 before the browser opens, with one SETUP line listing which journeys need which device. |
| Device measured | (after launch, before sign-in) | Reads the viewport, navigator.userAgent and maxTouchPoints on about:blank and prints them in the run header. A mismatch exits 2 before any sign-in. The device is also written to the findings file and to each walk. |
| Offline / online | step action { "do": "offline" } / { "do": "online" } |
Calls context.setOffline(true / false). The legacy placeholder (a wait whose label contains context.setOffline(true|false)) is also honoured, with a WARN asking for it to be rewritten. |
| Back online at the end | (after every journey, and before the sign-out) | A journey still offline at the end is put back online. If it walked every step, it gets a setup INVALID row ("never declared online"). The session release goes online before it calls /xas/ logout, so an offline run cannot leak a licence session. |
| GPS fix | "geolocation": { latitude, longitude, accuracy } on a step or on the journey; {{seed}} placeholders allowed |
Grants the browser's geolocation permission once, then calls context.setGeolocation(...) before the step's actions. The fix holds until the next one. Permission and position are cleared between journeys. An unresolved placeholder, a value that is not a number, or an out-of-range coordinate is INVALID and stops the journey. It is never sent as 0,0. |
_geolocation |
same shape | Read when geolocation is absent, for files written before the runner supported the field. It prints a WARN: validate-journeys.js treats _ keys as comments and does not check their placeholders. |
New exports, for the selftest: resolveDevice, runDevice, contextOptions, deviceLabel, judgeDevice, networkVerb,
setNetwork, isOffline, ensureOnline, networkEndRow, resolveGeolocation, applyGeolocation, clearGeolocation,
act, describeAct.
Evidence
- Selftest:
node journey-runner.selftest.jspasses 56 of 56 checks, with no app, no browser and no sign-in. Act runs
against a fake page whoselocator()throws, which proves a network step never touches the page. The checks that must
fail do fail: an unknown device, a mixed run, a 1440x900 context for a phone journey, a desktop user agent or no touch on
a phone, an unresolved{{placeholder}}, latitude 95, an empty-string latitude. Static checks over the project's 57
journey files: every declared device opens 390x844; 4 files with offline steps all end online; 10 GPS fixes use only
seeds their own file declares. - Real browser, no app: headless Chromium with the runner's phone options reported viewport 390x844, the iPhone user
agent,maxTouchPoints1,pointer: coarse, andinnerWidth390 on a page with a viewport meta tag. Against a
throwawayhttp://localhostserver, the position read was "User denied" before the fix, the test coordinates after it,
the new value after a second fix, and "User denied" again after the clear. An offline reload failed with
net::ERR_INTERNET_DISCONNECTED, and the reload afterensureOnlineworked. - Not measured when the patch was written: a live run against a Mendix app.
- Observed, for whoever picks this up: on
about:blankChromium refuses geolocation ("Only secure origins are
allowed").http://localhostcounts as a secure origin, so a local app works. An app on a plain-http hostname other
than localhost will not get a position.
Asks
- Take the runner patch, or something with the same contract. We can open it as a PR against
project-tests/e2e/journey-runner.jsand its selftest. validate-journeys.js: addofflineandonlinetoACTIONS, which today rejects them with "unknown action … the
runner throws". Placeholders ingeolocationare already checked (the key has no_prefix). Optionally flag
_geolocationand the legacy labelled wait as "rename to the supported field". (Detail, plus a step-tag check, in the
companion validator issue.)- Document
device,viewport,JOURNEY_DEVICE,offline/onlineandgeolocationinskills/journey-examples.md,
together with the one-device-per-run rule.
Activity
fix(validate-journeys): accept offline/online, check device/viewport/…
new(journeys): phone device per run, offline/online steps, GPS fix pe…
Bug sweep 2026-10-26: gates, runners, doctor, lint receipt, git hooks…
MendixMau commentedon Oct 26, 2026
Fixed in #578 (squash a088b4e). The patch wasn't attached, so this was written from the issue's contract (credited to you). It adds:
device,viewport,JOURNEY_DEVICE); a mixed-device run exits 2;offline/onlinesteps;geolocationper step or per journey.All opt-in. Tested in headless Chromium, not yet on a live Mendix app.
Generated by Claude Code