Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions docs/CHALLENGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6832,6 +6832,71 @@ since it snaps to steps of 10; Balance, which is continuous, landed at 0.28
for 0.25, because a finger on the thumb is not the thumb's center, the
inset Android's seek bars needed (227). A tap on the track moves nothing.

### 319. A web page's range was a text field to `map` and to `fill`

**Found by:** giving MobiumApp's Slider Demo a web page's
`<input type="range">` (0 to 100 in steps of 10) beside its native sliders,
to measure every kind of slider on iOS (ROADMAP). `map` printed it as
`50 (input)` — named by its value, the mistake CHALLENGES 65, 219 and 234
fixed natively — and `app_fill` typed into it: `fill 20` worked, while
`fill 0.2`, the position every native slider takes, was clamped by the
page to a step and failed as "the page rewrote or refused the value", a
cause that was not the case. `app_type` was accepted too, where a native
slider refuses it. **Fixed:** a range is a slider. `map` names it by its
label and reads its value as its state — `Range, 0 to 100 in steps of 10
(slider, 50)` — and `role=slider` finds it. `app_fill` takes a position from
0 to 1, as on every other slider, and anything else is refused before
anything moves; `app_type` is refused naming `app_fill`. Unlike a native
slider (219), a page gives its range's `min`, `max` and `step`, so the
position is turned into the value the page would take and set through the
native setter with the `input` and `change` events a drag causes, and it
lands exactly: on the iPhone 17 Pro simulator 0.2, 1, 0, 0.35 and 0.5 read
20, 100, 0, 40 and 50, and the page's own output followed each.

**And from the native side, on Android.** Android's WebView draws the range
as a `SeekBar` — but where a native seek bar's text is its progress (227),
Chrome's carries the range's label and no progress at all, so `map` in
`NATIVE_APP` printed `SeekBar (slider, Range, 0 to 100 in steps of 10 in its
own steps)`: named by its class and valued by its name. A seek bar inside a
WebView now keeps its text as its name and has no value, and `app_fill` on
it goes through the page when the page can be reached, as a field in a page
does (301) — on the Android 15 emulator, 0.3 read "30", through its page.
Adding these rows to `slider.sh` found it mapping the screen before the
Slider Demo had opened, with nothing waited for — CHALLENGES 233's race,
which had passed by timing; it waits for the screen now. `slider.sh` passed
three times in three on the emulator and `slider-ios.sh` three in three on
the simulator, which failed at the range with the binary before this fix.

### 320. A two-thumb slider built from ARIA was not in `map`, and one built from ranges moved the wrong thumb

**Found by:** measuring two-thumb sliders, as pages build them, on
MobiumApp's Slider Demo: Price, two `<input type="range">` laid over one
track, the minimum kept at or below the maximum by the page; and Hours, two
`role="slider"` thumbs on a drawn track, moved by a finger or the arrow
keys, the way component libraries build one. Hours was not in `map` at
all: the list of a page's elements had no `[role=slider]`. Price mapped as
two sliders by their `aria-label`s, as 319 made them, but filling the
maximum below the minimum moved the **minimum**, and `fill` reported the
maximum's value as landed: the page decides which thumb gives way by which
has focus, and the fill set a value without focusing — where a finger on a
thumb focuses it. **Fixed:** a range is focused before it is moved, so the
page holds the thumb that was not moved, and the fill fails as
`not_confirmed`, saying the page rewrote the value. An ARIA slider is in
`map` by its label, with `aria-valuetext` or `aria-valuenow` as its value
— `Start hour (slider, 9:00)` — and `app_fill` takes a position from 0 to 1
of `aria-valuemin` to `aria-valuemax`, the one rule for every slider, and
moves it with its arrow keys, which WAI-ARIA requires a slider to answer,
reading `aria-valuenow` back after each: it stops on the value, or between
two steps at the closer, or — another thumb in the way — fails, saying the
page stopped it. One whose keys move nothing is refused as unsupported,
naming a drag. On the iPhone 17 Pro simulator and the Android 15 emulator,
0.25 and 0.5 of 24 hours landed on 6:00 and 12:00, and the start pushed
toward 24 stopped at the end's 12:00 and said so; `slider-ios.sh` and
`slider.sh` passed three times in three on each, and the iOS check failed
at the missing ARIA sliders with the binary before. Native two-thumb
sliders — Material's `RangeSlider`, Compose's, an iOS app's own — are not
measured yet (ROADMAP).

## Findings that were not defects

Worth recording because each one closed off an approach that looked obvious.
Expand Down
4 changes: 3 additions & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,9 @@ this is what is not.
a fill that lands exactly from partway, which WebDriverAgent cannot
support — it reports no thumb position, and its range is real for a
UIKit slider and 0 to 1 for SwiftUI's (CHALLENGES 219, 2026-10-13);
range sliders; and the phone's text
two-thumb sliders natively — Material's RangeSlider, Compose's, an iOS
app's own (a page's, two ranges or two ARIA sliders, since 2026-10-13,
CHALLENGES 320); and the phone's text
size, which is a slider in Settings. A custom control that does carry the
Adjustable trait — Pocket Casts' scrubber — maps as `adjustable` with its
value since 2026-10-05 (CHALLENGES 234), and `app_fill` refuses it,
Expand Down
3 changes: 2 additions & 1 deletion docs/checks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ checks do not: `clean-stop.sh`, which stops the caller's daemon, and
| [pocketcasts-ios.sh](pocketcasts-ios.sh) | **A fourth third-party app on iOS**: Pocket Casts, a podcast player, signed out, built from its source for a simulator or from the App Store on a phone, and CHALLENGES 234–239 held against it: onboarding's button mapped disabled and `map --diff` saying when it is enabled, a row's checkmark not a target of its own, Discover's rows and a podcast's header named by their words rather than the buttons and images inside them, Play touched clear of the divider through its center, the player's first-run tip said by `map` and `app_alert` and closed where they say, and the scrubber mapped as adjustable with `app_fill` refused. On a simulator, iPhone or iPad, the app's data is cleared first; a phone's onboarding and tip run only on a fresh install, and the app is first brought back to Discover from wherever it was left. An episode is played for a moment and paused |
| [kiwix-ios.sh](kiwix-ios.sh) | **A fifth third-party app on iOS, and the first whose WebView opens**: Kiwix, the offline Wikipedia reader, built from its source for a simulator or from the App Store on an iPhone, and CHALLENGES 244–248 and 256 held against it with a pinned ZIM: each catalog card one entry, off-screen ambiguity told to name one, an article tile one link named once, the disabled List button refused by its own ref, and a tap in the WebView — taller than its page, under the bars — opening the article it names; a page under the Library neither mapped nor tapped. A simulator starts from cleared data, with the ZIM uploaded and opened through the system document picker; a phone keeps its data, so the ZIM must already be in Kiwix's library from its own download, and 245 is reached only with no page under the Library |
| [feed.sh](feed.sh) | **A list that grows as it is scrolled**, on MobiumApp's Feed Demo: twenty rows a page, the next 2.5 seconds after the end is reached, three pages in all. `scroll-to` reaches Row 55 across two loads instead of stopping at the first page's end, says Row 70 is past the real end at once, and an unknown role is refused rather than waited on (CHALLENGES 222, 223). Passed on the simulator, the iPhone and the Pixel 8 Pro |
| [slider.sh](slider.sh) | **Sliders on Android**, on MobiumApp's Slider Demo: Volume (0–100 in tens) and Balance (0–1, continuous) map as sliders by name with a value, are filled by position and land exactly — Volume on 30, 100 and 0, Balance on 0.25 — `map --diff` says the value moved, and a non-number and `type` are refused (CHALLENGES 227). Passed on the Pixel 8 Pro. Android only: on iOS the demo's sliders report no slider at all |
| [slider.sh](slider.sh) | **Sliders on Android**, on MobiumApp's Slider Demo: Volume (0–100 in tens) and Balance (0–1, continuous) map as sliders by name with a value, are filled by position and land exactly — Volume on 30, 100 and 0, Balance on 0.25 — `map --diff` says the value moved, and a non-number and `type` are refused (CHALLENGES 227); a web page's range, named by its label from the native side and filled through its page (319), and the two-thumb Price and Hours (320). Passed on the Pixel 8 Pro; with the web rows, 2026-10-13, three in three on the Android 15 emulator. Android only: on iOS the demo is [slider-ios.sh](slider-ios.sh) |
| [slider-ios.sh](slider-ios.sh) | **Sliders on iOS, three kinds**, on MobiumApp's Slider Demo: Volume, a plain view to iOS (`map` calls it a button), where `fill` is refused as unsupported naming `swipe` and a drag lands it (CHALLENGES 318); Level, the same slider made accessible, mapped as adjustable with its value, `fill` refused naming a drag and the drag landing it (234); and a web page's range, a slider by its label, filled by position and landing exactly from the page's min, max and step, with a value and `type` refused (319); and two two-thumb sliders, Price (two ranges) and Hours (two ARIA sliders), each thumb a slider, a crossing stopped by the page and said so (320). Passed 2026-10-13 three times in three on the iPhone 17 Pro simulator, about 30 seconds; it failed at the range with the binary before 319 and at the ARIA sliders with the one before 320 |
| [frames.sh](frames.sh) | **Frames inside a WebView**, on MobiumApp's Frames page: in the WebView's context every frame's elements are mapped with their frame — same-origin and nested through the page, cross-origin through its own execution context — each button tapped reaches its frame, the cross-origin card field is filled and added to as the frame reports, and `NATIVE_APP` reaches the cross-origin button too (CHALLENGES 228, 229). Passed on the iPhone simulator and an emulator |
| [compose-app.sh](compose-app.sh) | **A Jetpack Compose app**, Seal from F-Droid, the first Compose app driven here, and CHALLENGES 176–178 held against it. Its icon buttons map once each as buttons. Its first-launch dialog, which the platform's alert endpoint does not know, is read by `alert` and not accepted, a tap on what it covers is refused naming it, and a rule answers it by its Close button. A switch row is set by its words, read back and put back, and a row with no state is refused. An app the check installed is uninstalled after, and on a phone nothing read off it is printed. Emulators and phones |
| [map-diff.sh](map-diff.sh) | **`map --diff`**, on MobiumApp's Form Demo. The first diff of a session says it had nothing to compare with and adds the whole screen. A map with nothing done between says nothing changed, the control that it can come back empty. A box checked is its state and what it was, and on iOS, where its row grows, what is under it moves as one line, not five. A screen left is its elements gone and the next screen's added, and a ref from the diff acts. Emulators and simulators |
Expand Down
121 changes: 121 additions & 0 deletions docs/checks/slider-ios.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/bin/sh
# Sliders on iOS, on MobiumApp's Slider Demo: three kinds, each handled the
# way the one fill rule says — a slider is filled with a position from 0 to
# 1, and a control Mobium cannot set to a position is refused, naming a drag.
#
# docs/checks/slider-ios.sh <simulator-udid|iphone-udid>
#
# - Volume, @react-native-community/slider as shipped: on iOS a plain view
# with no trait and no value, so map calls it a button. app_fill is
# refused as unsupported, naming app_swipe, with nothing moved
# (CHALLENGES 318); a drag on its thumb lands it, in steps of 10.
# - Level, the same slider made accessible: the Adjustable trait and a
# value. Mapped as adjustable with its value; app_fill refused naming a
# drag (234); the drag lands it and map reads the new value.
# - Range, a web page's <input type="range">, 0 to 100 in steps of 10: a
# slider by its label, filled by position, landing exactly from the
# page's own min, max and step; a value and app_type refused (319).
#
# Android's sliders are slider.sh. Real UIKit and SwiftUI sliders, which
# app_fill moves (219), are icecubes-ios.sh.
set -e
DEV="$1"
if [ -z "$DEV" ]; then echo "usage: $0 <simulator-udid|iphone-udid>" >&2; exit 2; fi
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
. "$ROOT/docs/checks/lib.sh"
check_platform ios "$DEV"
check_lock "$DEV"
M="$ROOT/bin/mobium --device $DEV"
APP=dev.mobium.mobiumapp
fail() { echo "FAIL: $*" >&2; exit 1; }
row() { printf ' %-10s %-60s ok\n' "$1" "$2"; }

# refused <exit> <words> <command...>: refused with that exit status, saying so.
refused() {
want=$1 words=$2; shift 2
set +e; out=$("$@" 2>&1); got=$?; set -e
[ "$got" = "$want" ] || fail "$*: exit $got, want $want: $out"
echo "$out" | grep -q "$words" || fail "$*: refused without saying \"$words\": $out"
}
state() { $M text testid=sliderState; }
# The track's bounds in device pixels, from map: x1 x2 y.
track() { $M map --json | python3 -c '
import json, sys
for e in json.load(sys.stdin)["elements"]:
if e["label"] == sys.argv[1]:
b = e["bounds"]; print(b["x1"], b["x2"], (b["y1"] + b["y2"]) // 2); break' "$1"; }
# drag <label> <from> <to>: a finger on the thumb at one fraction of the
# track, moved to another.
drag() {
set -- "$1" "$2" "$3" $(track "$1")
[ -n "$6" ] || fail "no bounds for $1 in map"
x() { python3 -c "print(int($2 + $1 * ($3 - $2)))"; }
$M swipe "$(x "$2" "$4" "$5")" "$6" "$(x "$3" "$4" "$5")" "$6" --duration 800ms >/dev/null
}

echo "--- $DEV"
$M terminate "$APP" >/dev/null 2>&1 || true
out=$($M launch "$APP" 2>&1) || fail "launch: $out"
$M scroll-to 'text="Slider Demo"' --direction down >/dev/null || fail "MobiumApp has no Slider Demo — rebuild it"
$M tap 'text="Slider Demo"' >/dev/null
$M wait testid=sliderState --timeout 10s >/dev/null || fail "the Slider Demo did not open"
case "$(state)" in
"volume: 50, balance: 0.50, level: 5, changes: 0") ;;
*) fail "the Slider Demo has no Level, or did not start fresh: $(state) — rebuild MobiumApp" ;;
esac

map=$($M map)
echo "$map" | grep -q " Volume (button)" || fail "Volume is no longer a plain view to map — has the library changed? $map"
echo "$map" | grep -q " Level (adjustable, 50%)" || fail "Level is not adjustable at 50%: $map"
row "map" "Volume a plain view (button); Level adjustable, 50%"

refused 5 "not a text field or a slider" $M fill testid=volumeSlider 0.2
echo "$(state)" | grep -q "^volume: 50," || fail "a refused fill moved Volume: $(state)"
drag Volume 0.5 0.2
echo "$(state)" | grep -q "^volume: 20," || fail "a drag from 50% to 20% left $(state)"
row "volume" "fill refused, naming app_swipe; a drag lands it on 20"

refused 2 "app_swipe" $M fill testid=levelSlider 0.8
echo "$(state)" | grep -q "level: 5," || fail "a refused fill moved Level: $(state)"
drag Level 0.5 0.8
echo "$(state)" | grep -q "level: 8," || fail "a drag from 50% to 80% left $(state)"
$M map | grep -q " Level (adjustable, 80%)" || fail "map does not read Level at 80%"
row "level" "fill refused, naming a drag; the drag lands it on 8"

WEB=$($M contexts | awk '/^WEBVIEW_/ {print $1; exit}')
[ -n "$WEB" ] || fail "the Slider Demo's page is not inspectable — rebuild MobiumApp"
at_exit "$M context NATIVE_APP >/dev/null 2>&1"
$M context "$WEB" >/dev/null
$M map | grep -q " Range, 0 to 100 in steps of 10 (slider, 50)" || fail "the range does not map as a slider by its label: $($M map)"
for p in "0.2 20" "1 100" "0 0" "0.35 40"; do
set -- $p
$M fill testid=webRange "$1" >/dev/null || fail "fill the range to $1"
$M text | grep -q "^range: $2\$" || fail "the range filled to $1 reads $($M text | tail -1), want $2"
done
refused 2 "from 0" $M fill testid=webRange 20
refused 2 "app_fill" $M type testid=webRange 20
$M text | grep -q "^range: 40\$" || fail "a refused fill or type moved the range"
row "range" "a slider by its label; 0.2, 1, 0, 0.35 land on 20, 100, 0, 40"

# Two thumbs on one track, as pages build them (CHALLENGES 320): two ranges
# laid over each other, and two ARIA sliders, which map did not list. Each
# thumb is its own slider. A thumb pushed past the other is the page's to
# stop, and fill says the page stopped it rather than claiming the position.
$M map | grep -q " Minimum price (slider, 200)" || fail "Price's thumbs are not two sliders: $($M map)"
$M map | grep -q " Start hour (slider, 9:00)" || fail "the ARIA sliders are not in map: $($M map)"
$M fill testid=priceMin 0.5 >/dev/null || fail "fill Price's minimum"
$M fill testid=priceMax 0.9 >/dev/null || fail "fill Price's maximum"
$M text | grep -q "^price: 500 to 900\$" || fail "Price reads $($M text | grep '^price')"
refused 7 "rewrote" $M fill testid=priceMax 0.3
$M text | grep -q "^price: 500 to 500\$" || fail "the maximum pushed below the minimum left $($M text | grep '^price') — the other thumb moved"
$M fill testid=hoursStart 0.25 >/dev/null || fail "fill the start hour"
$M fill testid=hoursEnd 0.5 >/dev/null || fail "fill the end hour"
$M text | grep -q "^hours: 6 to 12\$" || fail "Hours reads $($M text | grep '^hours')"
refused 7 "stopped it at 12:00" $M fill testid=hoursStart 1
row "two thumbs" "Price and Hours each two sliders; a crossing is the page's to stop"

row "refusals" "a value and app_type refused before moving"

$M context NATIVE_APP >/dev/null
$M terminate "$APP" >/dev/null 2>&1 || true
echo PASS
Loading
Loading