Test the app on the simulator
SkillDev toolsManually test/QA the Liftosaur app on the iOS simulator — drive the real UI, inspect live app state, and set up test data. Use when asked to test, QA, stress-test, verify a change works, reproduce a bug, or hunt for bugs in the running app.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Test the app on the simulator skill
What this skill tells your AI
The instructions your AI receives, as published by astashov/liftosaur in .claude/skills/test-app/SKILL.md and read by ahel’s review.
Drive the running app on the iOS simulator, and for anything non-trivial verify against the live app state, not just the screen. Many bugs are UI/data desyncs the screenshot won't show.
Three tools do the work:
ios-simulatorMCP — drive the UI (the app is bare RN, sotestIDs are queryable).scripts/dump-rn-state.js— read the live Redux state (globalThis.state).liftosaur-localMCP — set up the environment (programs, exercises, history, settings) on the same account the simulator is signed into.
Focus: $ARGUMENTS
Auth preflight (throwaway test account)
The simulator app and the liftosaur-local MCP must be on the SAME account. The MCP's API key
is static (in ~/.claude.json → this project's mcpServers.liftosaur-local.headers), and
debugLogin rebinds that key server-side to a freshly created account — one call aligns both.
- Get the key:
python3 -c "import json,os;c=json.load(open(os.path.expanduser('~/.claude.json')));print(c['projects']['/Users/anton/projects/liftosaur']['mcpServers']['liftosaur-local']['headers']['Authorization'].split()[-1])" - Check the current account:
node scripts/dump-rn-state.js --eval "globalThis.state.user". If it's already thetest_account you're mid-scenario with, skip step 3 — eachdebugLogincall starts a NEW empty account. - Create a fresh throwaway account (empty storage, active subscription — premium features work)
and rebind the MCP key to it:
node scripts/dump-rn-state.js --eval "globalThis.debugLogin('<key>')" # → {"userId":"test_xxxxxxxx","email":"test_xxxxxxxx@test.liftosaur.com","apiKeyBound":true} - Verify:
list_programsvia MCP should reflect the same empty account the sim now shows.
Requires the local dev server (npm run start:server) — the sign-in bypass and key-rebind
endpoint are Utils_isLocal()-gated, and globalThis.debugLogin exists only in __DEV__
builds. This signs the simulator out of whatever account it was on (that account's local data
stays on-device; a normal sign-in restores it) — leave the sim on the test account when done
and tell the user which account you left it on.
The core loop
- Set up the scenario via
liftosaur-localMCP (or by driving the UI). - Act in the UI via
ios-simulatorMCP — target bytestID, never pixel-guess. - Verify with
dump-rn-state.js: does the live state match what the screen shows and what you intended? A mismatch between control ↔ state ↔ intent is a bug. - Reset between cases (in-app undo/discard, or re-seed via MCP) so each test starts clean.
Nuclear option:
globalThis.debugLogin('<key>')again — brand-new empty account, MCP follows.
Tools
ios-simulator MCP (drive the UI)
get_booted_sim_idonce (bundle idcom.liftosaur.www).ui_find_element(["<testID>"])→ tap the returned frame center.testIDs surface asAXUniqueId. Prefer semantic targeting over coordinates.ui_view— cheap screenshot for a visual/state check.ui_describe_all— full a11y tree; use sparingly, it can exceed the token limit. If it's dumped to a file, pull the ids out:python3 -c "import re;print('\n'.join(dict.fromkeys(re.findall(r'\"AXUniqueId\":\"([^\"]+)\"',open('FILE').read()))))"ui_typeafter tapping a field;ui_swipeto scroll;launch_app(..., terminate_running:true)to relaunch. See mainCLAUDE.mdfor idb setup.
dump-rn-state.js (read live state)
node scripts/dump-rn-state.js storage.currentProgramId # dotted path
node scripts/dump-rn-state.js --eval "Object.keys(globalThis.state)" # arbitrary expr
node scripts/dump-rn-state.js --eval "globalThis.state.storage.settings.units"
Hermes' JSON.stringify drops undefined/functions, so you get clean IState. Useful roots:
storage (programs, settings, history, stats), progress (in-progress workout),
editProgramStates / editProgramExerciseStates (edit buffers). Needs Metro connected — a
"Fast Refresh disconnected" banner means it dropped; relaunch the app.
liftosaur-local MCP (set up the environment)
Same account as the sim. Read the DSL guide first — it is not guessable:
get_liftoscript_reference, then get_liftoscript_examples.
- Programs:
create_program/update_program(validate withrun_playgroundBEFORE saving),list_programs,get_program,delete_program. - Also: custom exercises/equipment/gyms,
create_history_record, measurements,set_exercise_data.
After creating/updating data via MCP, the device won't see it until it syncs — relaunch with
launch_app(bundle_id, terminate_running:true) to force a full sync, then switch to it in-app
(Me → Program) if needed.
Understand the target first
Before driving the UI, know the testIDs and the handlers behind the feature. Fastest path:
spawn an Explore agent to grep testID= in the relevant components and report each control,
its handler, and how it serializes/writes state (with file:line). Note handler asymmetries
(one path preserves a field, a sibling resets it) — those are prime bug candidates.
Native code lives in src/components/** (.tsx using RN primitives; check for .native.tsx
variants). State shapes are in src/types.ts and src/models/state.ts.
Gotchas (learned the hard way)
- Coordinates:
ui_find_elementframes are in full-scroll coords; taps use screen points. The tab bar sits at ~y≥762 and overlaps list rows — scroll the target above it before tapping, or you'll hit a footer tab. - Inputs commit on blur — after
ui_typeinto a numeric/text field, dismiss the keypad (its keyboard-icon) before dumping state, or the change won't be in state yet. - CodeMirror / inlined-HTML WebViews (e.g. the Liftoscript full-text editor) are ONE opaque a11y node — you can't tap inside. Set that content via the MCP instead.
- Reload to sync MCP-made data:
launch_app(..., terminate_running:true). - Verify the DATA, not just the screen — the screen can look right while state is wrong.
- Leave the account as you found it (or tell the user what you changed): switch the active program back, delete throwaway programs/history, and confirm nothing unintended was saved.
Reporting
When bug-hunting, write findings to lambda/scripts/bugs/<area>-<date>.md. Per bug: severity,
numbered repro steps, a before/after state dump proving it, any UI-vs-state mismatch, and the
file:line root cause. Reproduce from a clean state before reporting, and list what you verified
working too.
Example: program-editor QA
A worked scenario — reuse/repeat mechanics on the Edit Program screens — including a ready-made
fixture program and the exact bugs it surfaced, is in
lambda/scripts/bugs/program-editor-bugs-2026-07-01.md. Use it as a model for structuring a run.
Signals
- GitHub stars
- 708
- Forks
- 112
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
test-app- Source
- github.com/astashov/liftosaur