Skip to content

Commit 8b97b5e

Browse files
committed
feat: add skillgym tests
1 parent 5a3cf94 commit 8b97b5e

38 files changed

Lines changed: 9691 additions & 34 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ xcuserdata/
2828
*.app
2929
*.xctestrun
3030
*.xcarchive
31+
.skillgym-results/

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ For people:
8787

8888
- [Website](https://agent-device.dev/)
8989
- [Docs](https://incubator.callstack.com/agent-device/docs/introduction)
90+
- [Skillgym starter](test/skillgym/README.md)
91+
92+
Local benchmark starter:
93+
94+
- `pnpm test:skillgym`
9095

9196
For agents:
9297

examples/test-app/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.expo/
2+
node_modules/

examples/test-app/README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Agent Device Tester
2+
3+
`Agent Device Tester` is a minimal Expo Router fixture app for `agent-device` and `skillgym` experiments.
4+
5+
It is intentionally small, but each surface is dense with durable accessibility targets so a few screens cover a large share of the workflows we care about.
6+
7+
## Why this app exists
8+
9+
- It gives `agent-device` a stable React Native target that we control.
10+
- It makes `skillgym` prompts concrete: the agent can inspect real app files instead of answering against an imagined UI.
11+
- It keeps the number of screens low while still covering roughly 50 practical interaction and verification cases.
12+
13+
## Screens
14+
15+
- `Home`: visible-text checks, dismissible banner, modal open/close, async loading, status badge, switch state
16+
- `Catalog`: search debounce, filter chips, long-list scroll, favorite toggles, cart updates, drill-in navigation
17+
- `Product detail`: back navigation, quantity stepper, multiline notes, save action
18+
- `Checkout form`: required-field validation, fill vs type, checkbox state, choice groups, keyboard dismiss, success summary
19+
- `Settings`: switch rows, accordion content, loading and error states, retry flow, destructive-confirm modal
20+
21+
Navigation uses Expo Router native bottom tabs, so the tab bar itself is also part of the test surface.
22+
23+
## Coverage map
24+
25+
These are the main case families this app can support without adding more screens:
26+
27+
- app open and close
28+
- visible text verification with plain `snapshot`
29+
- interactive discovery with `snapshot -i`
30+
- `press` on stable buttons, pills, and rows
31+
- `fill` on single-line and multiline fields
32+
- `type` after focus for append flows
33+
- `get text` on headings, badges, summaries, and accordion content
34+
- `is visible` and `is exists` assertions
35+
- `wait` for async loading and success states
36+
- `diff snapshot` after dismissals and submits
37+
- long-list scrolling and `scrollintoview`
38+
- selector-based navigation across repeated cards
39+
- modal open, cancel, and confirm flows
40+
- switch and checkbox state changes
41+
- validation-error and recovery loops
42+
- retryable error banners
43+
- cart counters and quantity changes
44+
- screenshot and recording proof capture
45+
46+
## Run locally
47+
48+
From the repo root:
49+
50+
```bash
51+
pnpm test-app:install
52+
pnpm test-app:ios
53+
```
54+
55+
Or on Android:
56+
57+
```bash
58+
pnpm test-app:install
59+
pnpm test-app:android
60+
```
61+
62+
If you prefer to work from inside the app folder:
63+
64+
```bash
65+
cd examples/test-app
66+
pnpm install --ignore-workspace --lockfile=false
67+
pnpm ios
68+
```
69+
70+
Or on Android:
71+
72+
```bash
73+
cd examples/test-app
74+
pnpm install --ignore-workspace --lockfile=false
75+
pnpm android
76+
```
77+
78+
Once the app is running, use `agent-device` against `Agent Device Tester` like any other target app.

examples/test-app/app.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"expo": {
3+
"name": "Agent Device Tester",
4+
"slug": "agent-device-test-app",
5+
"version": "1.0.0",
6+
"orientation": "portrait",
7+
"userInterfaceStyle": "automatic",
8+
"newArchEnabled": true,
9+
"plugins": ["expo-router"],
10+
"ios": {
11+
"supportsTablet": true,
12+
"bundleIdentifier": "com.callstack.agentdevicelab"
13+
},
14+
"android": {
15+
"package": "com.callstack.agentdevicelab",
16+
"edgeToEdgeEnabled": true,
17+
"predictiveBackGestureEnabled": false
18+
}
19+
}
20+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { NativeTabs } from 'expo-router/unstable-native-tabs';
2+
3+
import { useLabState } from '../../src/lab-state';
4+
import { useAppColors } from '../../src/theme';
5+
6+
export default function TabsLayout() {
7+
const colors = useAppColors();
8+
const { cartCount, diagnosticsState } = useLabState();
9+
10+
return (
11+
<NativeTabs
12+
backgroundColor={colors.tabBar}
13+
badgeBackgroundColor={colors.accent}
14+
iconColor={{ default: colors.textSoft, selected: colors.accent }}
15+
labelStyle={{
16+
default: { color: colors.textSoft, fontSize: 11, fontWeight: '600' },
17+
selected: { color: colors.accent, fontSize: 11, fontWeight: '700' },
18+
}}
19+
tintColor={colors.accent}
20+
>
21+
<NativeTabs.Trigger name="index">
22+
<NativeTabs.Trigger.Icon md="home" sf="house.fill" />
23+
<NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
24+
</NativeTabs.Trigger>
25+
<NativeTabs.Trigger name="catalog">
26+
<NativeTabs.Trigger.Icon md="storefront" sf="square.grid.2x2.fill" />
27+
<NativeTabs.Trigger.Label>Catalog</NativeTabs.Trigger.Label>
28+
<NativeTabs.Trigger.Badge hidden={cartCount === 0}>
29+
{String(cartCount)}
30+
</NativeTabs.Trigger.Badge>
31+
</NativeTabs.Trigger>
32+
<NativeTabs.Trigger name="form">
33+
<NativeTabs.Trigger.Icon md="fact_check" sf="doc.text.fill" />
34+
<NativeTabs.Trigger.Label>Form</NativeTabs.Trigger.Label>
35+
</NativeTabs.Trigger>
36+
<NativeTabs.Trigger name="settings">
37+
<NativeTabs.Trigger.Icon md="settings" sf="gearshape.fill" />
38+
<NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
39+
<NativeTabs.Trigger.Badge hidden={diagnosticsState !== 'error'}>!</NativeTabs.Trigger.Badge>
40+
</NativeTabs.Trigger>
41+
</NativeTabs>
42+
);
43+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useRouter } from 'expo-router';
2+
3+
import { AppFrame } from '../../src/components';
4+
import { useLabState } from '../../src/lab-state';
5+
import { CatalogScreen } from '../../src/screens/CatalogScreen';
6+
7+
export default function CatalogRoute() {
8+
const router = useRouter();
9+
const state = useLabState();
10+
11+
return (
12+
<AppFrame>
13+
<CatalogScreen
14+
activeCategory={state.activeCategory}
15+
cart={state.cartCounts}
16+
favorites={new Set(state.favoriteIds)}
17+
onAddToCart={state.addToCart}
18+
onOpenDetails={(productId) => router.push(`/product/${productId}`)}
19+
onSearchDraftChange={state.setSearchDraft}
20+
onSelectCategory={state.setActiveCategory}
21+
onToggleFavorite={state.toggleFavorite}
22+
products={state.catalogProducts}
23+
searchDraft={state.searchDraft}
24+
/>
25+
</AppFrame>
26+
);
27+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { AppFrame } from '../../src/components';
2+
import { useLabState } from '../../src/lab-state';
3+
import { FormScreen } from '../../src/screens/FormScreen';
4+
5+
export default function FormRoute() {
6+
const state = useLabState();
7+
8+
return (
9+
<AppFrame>
10+
<FormScreen
11+
errors={state.formErrors}
12+
form={state.form}
13+
onChange={state.updateForm}
14+
onReset={state.resetForm}
15+
onSubmit={state.submitOrder}
16+
submittedSummary={state.submittedSummary}
17+
/>
18+
</AppFrame>
19+
);
20+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useRouter } from 'expo-router';
2+
3+
import { AppFrame } from '../../src/components';
4+
import { useLabState } from '../../src/lab-state';
5+
import { HomeScreen } from '../../src/screens/HomeScreen';
6+
7+
export default function HomeRoute() {
8+
const router = useRouter();
9+
const state = useLabState();
10+
11+
return (
12+
<AppFrame>
13+
<HomeScreen
14+
cartCount={state.cartCount}
15+
isOnline={state.isOnline}
16+
isRefreshing={state.isRefreshing}
17+
lastSyncLabel={state.lastSyncLabel}
18+
modalVisible={state.modalVisible}
19+
noticeVisible={state.noticeVisible}
20+
onDismissNotice={state.dismissNotice}
21+
onHideModal={state.hideModal}
22+
onOpenCatalog={() => router.navigate('/catalog')}
23+
onOpenForm={() => router.navigate('/form')}
24+
onOpenSettings={() => router.navigate('/settings')}
25+
onRefresh={state.refreshMetrics}
26+
onSetOnline={state.setIsOnline}
27+
onShowModal={state.showModal}
28+
/>
29+
</AppFrame>
30+
);
31+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { AppFrame } from '../../src/components';
2+
import { useLabState } from '../../src/lab-state';
3+
import { SettingsScreen } from '../../src/screens/SettingsScreen';
4+
5+
export default function SettingsRoute() {
6+
const state = useLabState();
7+
8+
return (
9+
<AppFrame>
10+
<SettingsScreen
11+
diagnosticsExpanded={state.diagnosticsExpanded}
12+
diagnosticsLoading={state.diagnosticsLoading}
13+
diagnosticsState={state.diagnosticsState}
14+
notificationsEnabled={state.notificationsEnabled}
15+
onConfirmReset={state.resetLabState}
16+
onHideResetModal={state.hideResetModal}
17+
onLoadDiagnostics={state.loadDiagnostics}
18+
onRetryDiagnostics={state.retryDiagnostics}
19+
onSetNotificationsEnabled={state.setNotificationsEnabled}
20+
onSetReducedMotionEnabled={state.setReducedMotionEnabled}
21+
onShowResetModal={state.showResetModal}
22+
onToggleDiagnostics={() => state.setDiagnosticsExpanded(!state.diagnosticsExpanded)}
23+
reducedMotionEnabled={state.reducedMotionEnabled}
24+
resetModalVisible={state.resetModalVisible}
25+
/>
26+
</AppFrame>
27+
);
28+
}

0 commit comments

Comments
 (0)