mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
fix: clean up maestro test reporter output (#935)
* fix: clean up maestro test reporter output * chore: enable expo build disk cache * refactor: simplify replay progress detail formatting * fix: surface replay runner recovery hints * fix: prioritize ios runner recovery hint * fix: avoid trailing punctuation in runner state hint * fix: keep internal cleanup scripts out of runner hints * chore: remove redundant maestro test app open flag * fix: make test app maestro flow self-contained * fix: simplify maestro test duration output * fix: refine maestro test summary output * fix: dim maestro live progress counters * test: clear test app state before maestro flow * test: update maestro reporter progress expectations * chore: remove maestro app open flag handling * fix: apply maestro reporter cleanup to default reporter
This commit is contained in:
committed by
GitHub
parent
8cc6bb8b92
commit
6ae0612ebc
@@ -127,18 +127,15 @@ server from its launcher.
|
|||||||
The Maestro prototype suite lives in `examples/test-app/maestro` and runs through
|
The Maestro prototype suite lives in `examples/test-app/maestro` and runs through
|
||||||
`agent-device replay --maestro`:
|
`agent-device replay --maestro`:
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm test-app:maestro:ios -- --open "Agent Device Tester"
|
|
||||||
pnpm test-app:maestro:android -- --open "Agent Device Tester"
|
|
||||||
```
|
|
||||||
|
|
||||||
When the development build is already open and connected to Metro, omit
|
|
||||||
`--open` and run the suite against the existing session:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm test-app:maestro:ios
|
pnpm test-app:maestro:ios
|
||||||
|
pnpm test-app:maestro:android
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The Maestro flow includes `launchApp`, so the suite launches the app inside each
|
||||||
|
test attempt. Start Metro first when the installed development build needs the
|
||||||
|
local bundle.
|
||||||
|
|
||||||
The suite intentionally covers the compat layer syntax used by public Maestro suites:
|
The suite intentionally covers the compat layer syntax used by public Maestro suites:
|
||||||
`runFlow` file/inline blocks, `when.platform`, config hooks, deterministic `repeat.times`,
|
`runFlow` file/inline blocks, `when.platform`, config hooks, deterministic `repeat.times`,
|
||||||
flow `env`, selectors, input, assertions, and swipe.
|
flow `env`, selectors, input, assertions, and swipe.
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
|
"buildCacheProvider": {
|
||||||
|
"plugin": "expo-build-disk-cache"
|
||||||
|
},
|
||||||
"plugins": ["expo-router"],
|
"plugins": ["expo-router"],
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ appId: com.callstack.agentdevicelab
|
|||||||
env:
|
env:
|
||||||
CHECKOUT_NAME: Ada Lovelace
|
CHECKOUT_NAME: Ada Lovelace
|
||||||
CHECKOUT_EMAIL: ada@example.com
|
CHECKOUT_EMAIL: ada@example.com
|
||||||
PICKUP_TAPS: "2"
|
PICKUP_TAPS: '2'
|
||||||
onFlowStart:
|
onFlowStart:
|
||||||
|
- launchApp:
|
||||||
|
clearState: true
|
||||||
- assertVisible: Agent Device Tester
|
- assertVisible: Agent Device Tester
|
||||||
onFlowComplete:
|
onFlowComplete:
|
||||||
- assertVisible: Delivery choices
|
- assertVisible: Delivery choices
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
---
|
---
|
||||||
|
- scrollUntilVisible:
|
||||||
|
element:
|
||||||
|
id: home-open-form
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: home-open-form
|
id: home-open-form
|
||||||
- assertVisible: Checkout form
|
- assertVisible: Checkout form
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.2.2",
|
"@types/react": "~19.2.2",
|
||||||
|
"expo-build-disk-cache": "^0.7.4",
|
||||||
"typescript": "~6.0.3"
|
"typescript": "~6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+119
-1
@@ -59,6 +59,9 @@ importers:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ~19.2.2
|
specifier: ~19.2.2
|
||||||
version: 19.2.14
|
version: 19.2.14
|
||||||
|
expo-build-disk-cache:
|
||||||
|
specifier: ^0.7.4
|
||||||
|
version: 0.7.4(@expo/cli@56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3))(@expo/config@56.0.9(typescript@6.0.3))(typescript@6.0.3)
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ~6.0.3
|
specifier: ~6.0.3
|
||||||
version: 6.0.3
|
version: 6.0.3
|
||||||
@@ -1216,6 +1219,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
callsites@3.1.0:
|
||||||
|
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
camelcase@6.3.0:
|
camelcase@6.3.0:
|
||||||
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
|
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1314,6 +1321,15 @@ packages:
|
|||||||
core-js-compat@3.49.0:
|
core-js-compat@3.49.0:
|
||||||
resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==}
|
resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==}
|
||||||
|
|
||||||
|
cosmiconfig@9.0.2:
|
||||||
|
resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.9.5'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -1405,6 +1421,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
env-paths@2.2.1:
|
||||||
|
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
error-ex@1.3.4:
|
||||||
|
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
||||||
|
|
||||||
error-stack-parser@2.1.4:
|
error-stack-parser@2.1.4:
|
||||||
resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==}
|
resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==}
|
||||||
|
|
||||||
@@ -1438,6 +1461,13 @@ packages:
|
|||||||
react: '*'
|
react: '*'
|
||||||
react-native: '*'
|
react-native: '*'
|
||||||
|
|
||||||
|
expo-build-disk-cache@0.7.4:
|
||||||
|
resolution: {integrity: sha512-mU1NaPC2kAwUZ0bVjbuRHQ+FBKnlFvNwu6BR1ZKPRV9c7McvL7Ir1V5B08VvzTDtEMpzYjhyITPSU6loO9Bsew==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@expo/cli': '*'
|
||||||
|
'@expo/config': '*'
|
||||||
|
|
||||||
expo-constants@56.0.18:
|
expo-constants@56.0.18:
|
||||||
resolution: {integrity: sha512-8AMtbDGl/WVPnWlmbpGmvcdnNCy9E4PFnwdVwj600vljkMDPSxcAcjw8GVXEPk3PpZ+ngTqsrkltWyj0UKYAxw==}
|
resolution: {integrity: sha512-8AMtbDGl/WVPnWlmbpGmvcdnNCy9E4PFnwdVwj600vljkMDPSxcAcjw8GVXEPk3PpZ+ngTqsrkltWyj0UKYAxw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1735,6 +1765,10 @@ packages:
|
|||||||
engines: {node: '>=16.x'}
|
engines: {node: '>=16.x'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
import-fresh@3.3.1:
|
||||||
|
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
indent-string@4.0.0:
|
indent-string@4.0.0:
|
||||||
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
|
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1745,6 +1779,9 @@ packages:
|
|||||||
invariant@2.2.4:
|
invariant@2.2.4:
|
||||||
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
|
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
|
||||||
|
|
||||||
|
is-arrayish@0.2.1:
|
||||||
|
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||||
|
|
||||||
is-arrayish@0.3.4:
|
is-arrayish@0.3.4:
|
||||||
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
|
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
|
||||||
|
|
||||||
@@ -1806,6 +1843,9 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
json-parse-even-better-errors@2.3.1:
|
||||||
|
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||||
|
|
||||||
json5@2.2.3:
|
json5@2.2.3:
|
||||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1900,6 +1940,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
lines-and-columns@1.2.4:
|
||||||
|
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||||
|
|
||||||
lodash.debounce@4.0.8:
|
lodash.debounce@4.0.8:
|
||||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||||
|
|
||||||
@@ -2058,6 +2101,11 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
nanoid@3.3.15:
|
||||||
|
resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
|
||||||
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
negotiator@0.6.3:
|
negotiator@0.6.3:
|
||||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -2115,6 +2163,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==}
|
resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
parent-module@1.0.1:
|
||||||
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
parse-json@5.2.0:
|
||||||
|
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
parse-png@2.1.0:
|
parse-png@2.1.0:
|
||||||
resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==}
|
resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -2343,6 +2399,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
resolve-from@4.0.0:
|
||||||
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
resolve-from@5.0.0:
|
resolve-from@5.0.0:
|
||||||
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
|
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2727,6 +2787,9 @@ packages:
|
|||||||
zod@3.25.76:
|
zod@3.25.76:
|
||||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
|
zod@4.4.3:
|
||||||
|
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@adobe/css-tools@4.5.0': {}
|
'@adobe/css-tools@4.5.0': {}
|
||||||
@@ -4233,6 +4296,8 @@ snapshots:
|
|||||||
|
|
||||||
bytes@3.1.2: {}
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
|
callsites@3.1.0: {}
|
||||||
|
|
||||||
camelcase@6.3.0: {}
|
camelcase@6.3.0: {}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001787: {}
|
caniuse-lite@1.0.30001787: {}
|
||||||
@@ -4346,6 +4411,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.28.2
|
browserslist: 4.28.2
|
||||||
|
|
||||||
|
cosmiconfig@9.0.2(typescript@6.0.3):
|
||||||
|
dependencies:
|
||||||
|
env-paths: 2.2.1
|
||||||
|
import-fresh: 3.3.1
|
||||||
|
js-yaml: 4.1.1
|
||||||
|
parse-json: 5.2.0
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 6.0.3
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -4402,6 +4476,12 @@ snapshots:
|
|||||||
|
|
||||||
encodeurl@2.0.0: {}
|
encodeurl@2.0.0: {}
|
||||||
|
|
||||||
|
env-paths@2.2.1: {}
|
||||||
|
|
||||||
|
error-ex@1.3.4:
|
||||||
|
dependencies:
|
||||||
|
is-arrayish: 0.2.1
|
||||||
|
|
||||||
error-stack-parser@2.1.4:
|
error-stack-parser@2.1.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
stackframe: 1.3.4
|
stackframe: 1.3.4
|
||||||
@@ -4429,6 +4509,16 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
|
expo-build-disk-cache@0.7.4(@expo/cli@56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3))(@expo/config@56.0.9(typescript@6.0.3))(typescript@6.0.3):
|
||||||
|
dependencies:
|
||||||
|
'@expo/cli': 56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3)
|
||||||
|
'@expo/config': 56.0.9(typescript@6.0.3)
|
||||||
|
cosmiconfig: 9.0.2(typescript@6.0.3)
|
||||||
|
env-paths: 2.2.1
|
||||||
|
zod: 4.4.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- typescript
|
||||||
|
|
||||||
expo-constants@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3)):
|
expo-constants@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@expo/env': 2.3.0
|
'@expo/env': 2.3.0
|
||||||
@@ -4755,6 +4845,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
queue: 6.0.2
|
queue: 6.0.2
|
||||||
|
|
||||||
|
import-fresh@3.3.1:
|
||||||
|
dependencies:
|
||||||
|
parent-module: 1.0.1
|
||||||
|
resolve-from: 4.0.0
|
||||||
|
|
||||||
indent-string@4.0.0: {}
|
indent-string@4.0.0: {}
|
||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
@@ -4763,6 +4858,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
|
|
||||||
|
is-arrayish@0.2.1: {}
|
||||||
|
|
||||||
is-arrayish@0.3.4: {}
|
is-arrayish@0.3.4: {}
|
||||||
|
|
||||||
is-core-module@2.16.1:
|
is-core-module@2.16.1:
|
||||||
@@ -4820,6 +4917,8 @@ snapshots:
|
|||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
|
|
||||||
|
json-parse-even-better-errors@2.3.1: {}
|
||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
||||||
kleur@3.0.3: {}
|
kleur@3.0.3: {}
|
||||||
@@ -4884,6 +4983,8 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.32.0
|
lightningcss-win32-arm64-msvc: 1.32.0
|
||||||
lightningcss-win32-x64-msvc: 1.32.0
|
lightningcss-win32-x64-msvc: 1.32.0
|
||||||
|
|
||||||
|
lines-and-columns@1.2.4: {}
|
||||||
|
|
||||||
lodash.debounce@4.0.8: {}
|
lodash.debounce@4.0.8: {}
|
||||||
|
|
||||||
lodash.throttle@4.1.1: {}
|
lodash.throttle@4.1.1: {}
|
||||||
@@ -5129,6 +5230,8 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
nanoid@3.3.15: {}
|
||||||
|
|
||||||
negotiator@0.6.3: {}
|
negotiator@0.6.3: {}
|
||||||
|
|
||||||
negotiator@0.6.4: {}
|
negotiator@0.6.4: {}
|
||||||
@@ -5182,6 +5285,17 @@ snapshots:
|
|||||||
strip-ansi: 5.2.0
|
strip-ansi: 5.2.0
|
||||||
wcwidth: 1.0.1
|
wcwidth: 1.0.1
|
||||||
|
|
||||||
|
parent-module@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
callsites: 3.1.0
|
||||||
|
|
||||||
|
parse-json@5.2.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/code-frame': 7.29.0
|
||||||
|
error-ex: 1.3.4
|
||||||
|
json-parse-even-better-errors: 2.3.1
|
||||||
|
lines-and-columns: 1.2.4
|
||||||
|
|
||||||
parse-png@2.1.0:
|
parse-png@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pngjs: 3.4.0
|
pngjs: 3.4.0
|
||||||
@@ -5213,7 +5327,7 @@ snapshots:
|
|||||||
|
|
||||||
postcss@8.5.12:
|
postcss@8.5.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid: 3.3.11
|
nanoid: 3.3.15
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
@@ -5447,6 +5561,8 @@ snapshots:
|
|||||||
|
|
||||||
require-directory@2.1.1: {}
|
require-directory@2.1.1: {}
|
||||||
|
|
||||||
|
resolve-from@4.0.0: {}
|
||||||
|
|
||||||
resolve-from@5.0.0: {}
|
resolve-from@5.0.0: {}
|
||||||
|
|
||||||
resolve-workspace-root@2.0.1: {}
|
resolve-workspace-root@2.0.1: {}
|
||||||
@@ -5757,3 +5873,5 @@ snapshots:
|
|||||||
yargs-parser: 21.1.1
|
yargs-parser: 21.1.1
|
||||||
|
|
||||||
zod@3.25.76: {}
|
zod@3.25.76: {}
|
||||||
|
|
||||||
|
zod@4.4.3: {}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ const options = {
|
|||||||
platform: 'ios',
|
platform: 'ios',
|
||||||
session: 'test-app-maestro',
|
session: 'test-app-maestro',
|
||||||
flowDir: path.join(repoRoot, 'examples', 'test-app', 'maestro'),
|
flowDir: path.join(repoRoot, 'examples', 'test-app', 'maestro'),
|
||||||
openTarget: '',
|
|
||||||
close: false,
|
close: false,
|
||||||
passthrough: [],
|
passthrough: [],
|
||||||
};
|
};
|
||||||
@@ -37,11 +36,6 @@ for (let index = 2; index < process.argv.length; index += 1) {
|
|||||||
index += 1;
|
index += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (arg === '--open' && process.argv[index + 1]) {
|
|
||||||
options.openTarget = process.argv[index + 1];
|
|
||||||
index += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (arg === '--close') {
|
if (arg === '--close') {
|
||||||
options.close = true;
|
options.close = true;
|
||||||
continue;
|
continue;
|
||||||
@@ -67,14 +61,9 @@ function runAgentDevice(args) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.openTarget) {
|
|
||||||
runAgentDevice(['open', options.openTarget, '--platform', options.platform, ...options.passthrough]);
|
|
||||||
runAgentDevice(['wait', 'Agent Device Tester', '30000', '--platform', options.platform, ...options.passthrough]);
|
|
||||||
}
|
|
||||||
|
|
||||||
runAgentDevice([
|
runAgentDevice([
|
||||||
'test',
|
'test',
|
||||||
options.flowDir,
|
...flows,
|
||||||
'--maestro',
|
'--maestro',
|
||||||
'--platform',
|
'--platform',
|
||||||
options.platform,
|
options.platform,
|
||||||
|
|||||||
@@ -142,10 +142,11 @@ test('test command prints suite summary and exits non-zero on failures', async (
|
|||||||
assert.doesNotMatch(result.stderr, /Running replay suite\.\.\./);
|
assert.doesNotMatch(result.stderr, /Running replay suite\.\.\./);
|
||||||
assert.doesNotMatch(result.stdout, /✓ 01-pass\.ad \(0\.01s\)/);
|
assert.doesNotMatch(result.stdout, /✓ 01-pass\.ad \(0\.01s\)/);
|
||||||
assert.doesNotMatch(result.stdout, /⨯ "Checkout failure" in 02-fail\.ad/);
|
assert.doesNotMatch(result.stdout, /⨯ "Checkout failure" in 02-fail\.ad/);
|
||||||
|
assert.match(result.stdout, /Failures:\n Checkout failure\n file: 02-fail\.ad/);
|
||||||
assert.match(result.stdout, /Replay failed at step 1 \(open Demo\): boom/);
|
assert.match(result.stdout, /Replay failed at step 1 \(open Demo\): boom/);
|
||||||
assert.match(result.stdout, /artifacts: \/tmp\/test-artifacts\/02-fail/);
|
assert.match(result.stdout, /artifacts: \/tmp\/test-artifacts\/02-fail/);
|
||||||
assert.doesNotMatch(result.stdout, /SKIP \/tmp\/03-skip\.ad/);
|
assert.doesNotMatch(result.stdout, /SKIP \/tmp\/03-skip\.ad/);
|
||||||
assert.match(result.stdout, /Test summary: 1 passed, 1 failed in 0\.025s/);
|
assert.match(result.stdout, /Test summary: 1 passed \(3\), 1 failed in 0\.025s/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test command --verbose prints all test statuses', async () => {
|
test('test command --verbose prints all test statuses', async () => {
|
||||||
@@ -158,7 +159,44 @@ test('test command --verbose prints all test statuses', async () => {
|
|||||||
assert.doesNotMatch(result.stderr, /Running replay suite\.\.\./);
|
assert.doesNotMatch(result.stderr, /Running replay suite\.\.\./);
|
||||||
assert.doesNotMatch(result.stdout, /✓ 01-pass\.ad \(0\.01s\)/);
|
assert.doesNotMatch(result.stdout, /✓ 01-pass\.ad \(0\.01s\)/);
|
||||||
assert.doesNotMatch(result.stdout, /SKIP 03-skip\.ad/);
|
assert.doesNotMatch(result.stdout, /SKIP 03-skip\.ad/);
|
||||||
assert.match(result.stdout, /Test summary: 1 passed, 1 failed in 0\.025s/);
|
assert.match(result.stdout, /Test summary: 1 passed \(3\), 1 failed in 0\.025s/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test command colors suite summary segments when color is enabled', async () => {
|
||||||
|
const result = await runCliCapture(
|
||||||
|
['test', './suite'],
|
||||||
|
async () => ({
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
total: 1,
|
||||||
|
executed: 1,
|
||||||
|
passed: 1,
|
||||||
|
failed: 0,
|
||||||
|
skipped: 0,
|
||||||
|
notRun: 0,
|
||||||
|
durationMs: 25,
|
||||||
|
failures: [],
|
||||||
|
tests: [
|
||||||
|
{
|
||||||
|
file: '/tmp/01-pass.ad',
|
||||||
|
session: 'default:test:suite:1',
|
||||||
|
status: 'passed',
|
||||||
|
durationMs: 25,
|
||||||
|
attempts: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ env: { FORCE_COLOR: '1', NO_COLOR: undefined } },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.code, null);
|
||||||
|
assert.ok(
|
||||||
|
result.stdout.includes(
|
||||||
|
'Test summary: \u001B[32m1 passed\u001B[39m \u001B[2m(1)\u001B[22m in \u001B[33m0.025s\u001B[39m',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(result.stdout, /0 failed/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test command --verbose omits step telemetry for passing tests without debug mode', async () => {
|
test('test command --verbose omits step telemetry for passing tests without debug mode', async () => {
|
||||||
@@ -388,13 +426,13 @@ test('test command reports flaky passed-on-retry cases in the default summary',
|
|||||||
assert.doesNotMatch(result.stdout, /FLAKY/);
|
assert.doesNotMatch(result.stdout, /FLAKY/);
|
||||||
assert.doesNotMatch(
|
assert.doesNotMatch(
|
||||||
result.stdout,
|
result.stdout,
|
||||||
/^✓ "Authentication flow" in auth-flow\.yml \(passed attempt 17\.5s, total 112\.2s\)$/m,
|
/^✓ Authentication flow \(passed attempt 17\.5s, total 112\.2s\)$/m,
|
||||||
);
|
);
|
||||||
assert.match(result.stdout, /Test summary: 1 passed, 0 failed, 1 flaky in 0\.025s/);
|
assert.match(result.stdout, /Test summary: 1 passed \(1\), 1 flaky in 0\.025s/);
|
||||||
assert.match(result.stdout, /Flaky tests:/);
|
assert.match(result.stdout, /Flaky tests:/);
|
||||||
assert.match(
|
assert.match(
|
||||||
result.stdout,
|
result.stdout,
|
||||||
/✓ "Authentication flow" in auth-flow\.yml after 2 attempts \(passed attempt 17\.5s, total 112\.2s\)/,
|
/✓ Authentication flow after 2 attempts \(passed attempt 17\.5s, total 112\.2s\)/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
result.stdout,
|
result.stdout,
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { test } from 'vitest';
|
import { test } from 'vitest';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { formatReplayTestProgressEvent } from '../cli-test-progress.ts';
|
import {
|
||||||
|
createReplayTestProgressRenderer,
|
||||||
|
formatReplayTestProgressEvent,
|
||||||
|
} from '../cli-test-progress.ts';
|
||||||
import type { RequestProgressEvent } from '../daemon/request-progress.ts';
|
import type { RequestProgressEvent } from '../daemon/request-progress.ts';
|
||||||
|
|
||||||
function withStreamTty<T>(stream: NodeJS.WriteStream, isTTY: boolean, run: () => T): T {
|
function withStreamTty<T>(stream: NodeJS.WriteStream, isTTY: boolean, run: () => T): T {
|
||||||
@@ -70,7 +73,7 @@ test('formatReplayTestProgressEvent renders pass, retry, fail, and skip cases',
|
|||||||
maxAttempts: 2,
|
maxAttempts: 2,
|
||||||
durationMs: 12_345,
|
durationMs: 12_345,
|
||||||
},
|
},
|
||||||
expected: /^✓ 01-login\.ad \(12\.3s\)$/,
|
expected: /^✓ 01-login\.ad 12\.3s$/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
event: {
|
event: {
|
||||||
@@ -91,6 +94,7 @@ test('formatReplayTestProgressEvent renders pass, retry, fail, and skip cases',
|
|||||||
event: {
|
event: {
|
||||||
type: 'replay-test',
|
type: 'replay-test',
|
||||||
file: '/tmp/03-payment.ad',
|
file: '/tmp/03-payment.ad',
|
||||||
|
title: 'Payment flow',
|
||||||
status: 'fail',
|
status: 'fail',
|
||||||
index: 3,
|
index: 3,
|
||||||
total: 3,
|
total: 3,
|
||||||
@@ -98,11 +102,29 @@ test('formatReplayTestProgressEvent renders pass, retry, fail, and skip cases',
|
|||||||
maxAttempts: 2,
|
maxAttempts: 2,
|
||||||
durationMs: 9_876,
|
durationMs: 9_876,
|
||||||
message: 'assertVisible failed',
|
message: 'assertVisible failed',
|
||||||
|
hint: 'Stop the owning daemon and retry',
|
||||||
session: 'maestro-test:test:suite:3:attempt-2',
|
session: 'maestro-test:test:suite:3:attempt-2',
|
||||||
artifactsDir: '/tmp/replay-suite/payment',
|
artifactsDir: '/tmp/replay-suite/payment',
|
||||||
},
|
},
|
||||||
expected:
|
expected:
|
||||||
/^⨯ 03-payment\.ad \(9\.88s\)\n failed at: assertVisible failed\n session: maestro-test:test:suite:3:attempt-2\n artifacts: \/tmp\/replay-suite\/payment$/,
|
/^⨯ Payment flow 9\.88s\n file: 03-payment\.ad\n failed at: assertVisible failed\n hint: Stop the owning daemon and retry\n session: maestro-test:test:suite:3:attempt-2\n artifacts: \/tmp\/replay-suite\/payment$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: {
|
||||||
|
type: 'replay-test',
|
||||||
|
file: '/tmp/05-sharded.ad',
|
||||||
|
title: 'Sharded flow',
|
||||||
|
status: 'pass',
|
||||||
|
index: 5,
|
||||||
|
total: 6,
|
||||||
|
attempt: 1,
|
||||||
|
durationMs: 100,
|
||||||
|
shardIndex: 0,
|
||||||
|
shardCount: 2,
|
||||||
|
deviceId: 'emulator-5554',
|
||||||
|
deviceName: 'Pixel 8',
|
||||||
|
},
|
||||||
|
expected: /^✓ Sharded flow \[1\/2 Pixel 8\] 0\.1s$/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
event: {
|
event: {
|
||||||
@@ -142,7 +164,37 @@ test('formatReplayTestProgressEvent colors stderr progress rows when stdout is p
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(line, '\u001B[32m✓\u001B[39m 01-pass.ad (0.01s)');
|
assert.equal(line, '\u001B[32m✓\u001B[39m 01-pass.ad \u001B[33m0.01s\u001B[39m');
|
||||||
|
} finally {
|
||||||
|
if (typeof originalForceColor === 'string') process.env.FORCE_COLOR = originalForceColor;
|
||||||
|
else delete process.env.FORCE_COLOR;
|
||||||
|
if (typeof originalNoColor === 'string') process.env.NO_COLOR = originalNoColor;
|
||||||
|
else delete process.env.NO_COLOR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createReplayTestProgressRenderer dims live step progress when color is enabled', () => {
|
||||||
|
const originalForceColor = process.env.FORCE_COLOR;
|
||||||
|
const originalNoColor = process.env.NO_COLOR;
|
||||||
|
process.env.FORCE_COLOR = '1';
|
||||||
|
delete process.env.NO_COLOR;
|
||||||
|
try {
|
||||||
|
const renderer = createReplayTestProgressRenderer({ liveProgress: true });
|
||||||
|
const rendered = renderer.render({
|
||||||
|
type: 'replay-test',
|
||||||
|
file: '/tmp/checkout.yaml',
|
||||||
|
title: 'Checkout flow',
|
||||||
|
status: 'progress',
|
||||||
|
index: 1,
|
||||||
|
total: 1,
|
||||||
|
stepIndex: 3,
|
||||||
|
stepTotal: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(rendered, {
|
||||||
|
text: '\r\u001B[2K⊙ Checkout flow\u001B[2m [3/20]\u001B[22m',
|
||||||
|
newline: false,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (typeof originalForceColor === 'string') process.env.FORCE_COLOR = originalForceColor;
|
if (typeof originalForceColor === 'string') process.env.FORCE_COLOR = originalForceColor;
|
||||||
else delete process.env.FORCE_COLOR;
|
else delete process.env.FORCE_COLOR;
|
||||||
@@ -175,7 +227,7 @@ test('formatReplayTestProgressEvent colors completed result markers when color i
|
|||||||
attempt: 1,
|
attempt: 1,
|
||||||
durationMs: 10,
|
durationMs: 10,
|
||||||
}),
|
}),
|
||||||
'\u001B[32m✓\u001B[39m 01-pass.ad (0.01s)',
|
'\u001B[32m✓\u001B[39m 01-pass.ad \u001B[33m0.01s\u001B[39m',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
formatReplayTestProgressEvent({
|
formatReplayTestProgressEvent({
|
||||||
@@ -188,7 +240,7 @@ test('formatReplayTestProgressEvent colors completed result markers when color i
|
|||||||
attempt: 2,
|
attempt: 2,
|
||||||
durationMs: 30,
|
durationMs: 30,
|
||||||
}),
|
}),
|
||||||
'\u001B[33m✓\u001B[39m "Retry flow" in 02-flaky.yml (0.03s)',
|
'\u001B[33m✓\u001B[39m Retry flow \u001B[33m0.03s\u001B[39m',
|
||||||
);
|
);
|
||||||
const failedLine = formatReplayTestProgressEvent({
|
const failedLine = formatReplayTestProgressEvent({
|
||||||
type: 'replay-test',
|
type: 'replay-test',
|
||||||
@@ -201,7 +253,7 @@ test('formatReplayTestProgressEvent colors completed result markers when color i
|
|||||||
durationMs: 5,
|
durationMs: 5,
|
||||||
message: 'boom',
|
message: 'boom',
|
||||||
});
|
});
|
||||||
assert.ok(failedLine?.startsWith('\u001B[31m⨯\u001B[39m "Checkout failure" in 03-fail.ad'));
|
assert.ok(failedLine?.startsWith('\u001B[31m⨯\u001B[39m Checkout failure'));
|
||||||
} finally {
|
} finally {
|
||||||
if (typeof originalForceColor === 'string') process.env.FORCE_COLOR = originalForceColor;
|
if (typeof originalForceColor === 'string') process.env.FORCE_COLOR = originalForceColor;
|
||||||
else delete process.env.FORCE_COLOR;
|
else delete process.env.FORCE_COLOR;
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ test('readDaemonSocketProgressResponse parses split progress lines before respon
|
|||||||
assert.deepEqual(await responsePromise, { ok: true, data: { via: 'socket-progress' } });
|
assert.deepEqual(await responsePromise, { ok: true, data: { via: 'socket-progress' } });
|
||||||
assert.equal(socket.encoding, 'utf8');
|
assert.equal(socket.encoding, 'utf8');
|
||||||
assert.equal(socket.ended, true);
|
assert.equal(socket.ended, true);
|
||||||
assert.match(stderr, /✓ "Login flow" in 01-login\.ad \(1\.23s\)/);
|
assert.match(stderr, /✓ Login flow 1\.23s/);
|
||||||
} finally {
|
} finally {
|
||||||
process.stderr.write = originalStderrWrite;
|
process.stderr.write = originalStderrWrite;
|
||||||
}
|
}
|
||||||
@@ -237,11 +237,9 @@ test('readDaemonSocketProgressResponse rewrites live progress and clears it for
|
|||||||
socket.emit('data', `${progress(3)}\n${progress(4)}\n${pass}\n${responseLine}\n`);
|
socket.emit('data', `${progress(3)}\n${progress(4)}\n${pass}\n${responseLine}\n`);
|
||||||
|
|
||||||
assert.deepEqual(await responsePromise, { ok: true, data: { via: 'socket-progress' } });
|
assert.deepEqual(await responsePromise, { ok: true, data: { via: 'socket-progress' } });
|
||||||
assert.ok(stderr.includes('\r\u001B[2K⊙ "Tab View - Co..." in tab-view-coverflow.yml [3/10]'));
|
assert.ok(stderr.includes('\r\u001B[2K⊙ Tab View - Coverflow [3/10]'));
|
||||||
assert.ok(stderr.includes('\r\u001B[2K⊙ "Tab View - Co..." in tab-view-coverflow.yml [4/10]'));
|
assert.ok(stderr.includes('\r\u001B[2K⊙ Tab View - Coverflow [4/10]'));
|
||||||
assert.ok(
|
assert.ok(stderr.includes('\r\u001B[2K✓ Tab View - Coverflow 17.8s\n'));
|
||||||
stderr.includes('\r\u001B[2K✓ "Tab View - Coverflow" in tab-view-coverflow.yml (17.8s)\n'),
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
if (typeof originalCi === 'string') process.env.CI = originalCi;
|
if (typeof originalCi === 'string') process.env.CI = originalCi;
|
||||||
else delete process.env.CI;
|
else delete process.env.CI;
|
||||||
@@ -308,7 +306,7 @@ test('readDaemonSocketProgressResponse suppresses live progress outside interact
|
|||||||
socket.emit('data', `${progress}\n${pass}\n${responseLine}\n`);
|
socket.emit('data', `${progress}\n${pass}\n${responseLine}\n`);
|
||||||
|
|
||||||
assert.deepEqual(await responsePromise, { ok: true, data: { via: 'socket-progress' } });
|
assert.deepEqual(await responsePromise, { ok: true, data: { via: 'socket-progress' } });
|
||||||
assert.equal(stderr, '✓ "Tab View - Coverflow" in tab-view-coverflow.yml (17.8s)\n');
|
assert.equal(stderr, '✓ Tab View - Coverflow 17.8s\n');
|
||||||
} finally {
|
} finally {
|
||||||
process.stderr.write = originalStderrWrite;
|
process.stderr.write = originalStderrWrite;
|
||||||
}
|
}
|
||||||
|
|||||||
+81
-22
@@ -88,15 +88,15 @@ function formatReplayTestLiveProgressLine(
|
|||||||
): string {
|
): string {
|
||||||
const title = event.title?.trim();
|
const title = event.title?.trim();
|
||||||
const file = path.basename(event.file);
|
const file = path.basename(event.file);
|
||||||
const shardSuffix = formatReplayTestProgressShardSuffix(event);
|
const useColor = supportsColor(process.stderr);
|
||||||
const stepIndex = event.stepIndex ?? 0;
|
const shardSuffix = formatReplayTestProgressShardSuffix(event, { useColor });
|
||||||
const stepTotal = event.stepTotal ?? 0;
|
const stepSuffix = formatReplayTestLiveProgressStepSuffix(event, { useColor });
|
||||||
const suffix = `${shardSuffix} [${stepIndex}/${stepTotal}]`;
|
const suffix = `${shardSuffix}${stepSuffix}`;
|
||||||
const prefix = '⊙ ';
|
const prefix = '⊙ ';
|
||||||
if (!title) return trimToColumns(`${prefix}${file}${suffix}`, options.columns);
|
if (!title) return trimToColumns(`${prefix}${file}${suffix}`, options.columns);
|
||||||
|
|
||||||
const titlePrefix = `${prefix}"`;
|
const titlePrefix = prefix;
|
||||||
const titleSuffix = `" in ${file}${suffix}`;
|
const titleSuffix = suffix;
|
||||||
const availableTitleColumns = Math.max(
|
const availableTitleColumns = Math.max(
|
||||||
0,
|
0,
|
||||||
resolveColumns(options.columns) - titlePrefix.length - titleSuffix.length,
|
resolveColumns(options.columns) - titlePrefix.length - titleSuffix.length,
|
||||||
@@ -105,35 +105,79 @@ function formatReplayTestLiveProgressLine(
|
|||||||
return trimToColumns(`${titlePrefix}${formattedTitle}${titleSuffix}`, options.columns);
|
return trimToColumns(`${titlePrefix}${formattedTitle}${titleSuffix}`, options.columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatReplayTestLiveProgressStepSuffix(
|
||||||
|
event: ReplayTestCaseProgressEvent,
|
||||||
|
options: { useColor?: boolean } = {},
|
||||||
|
): string {
|
||||||
|
const stepIndex = event.stepIndex ?? 0;
|
||||||
|
const stepTotal = event.stepTotal ?? 0;
|
||||||
|
const suffix = ` [${stepIndex}/${stepTotal}]`;
|
||||||
|
return options.useColor ? colorizeProgressMarker(suffix, 'dim') : suffix;
|
||||||
|
}
|
||||||
|
|
||||||
function addReplayTestCaseDetailLines(
|
function addReplayTestCaseDetailLines(
|
||||||
lines: string[],
|
lines: string[],
|
||||||
event: ReplayTestCaseProgressEvent,
|
event: ReplayTestCaseProgressEvent,
|
||||||
options: ReplayTestProgressFormatOptions,
|
options: ReplayTestProgressFormatOptions,
|
||||||
): void {
|
): void {
|
||||||
if (options.verbose && event.status === 'fail') return;
|
if (shouldSuppressReplayTestCaseDetailLines(event, options)) return;
|
||||||
|
const fileLine = replayTestProgressFailureFileLine(event);
|
||||||
|
const messageLine = replayTestProgressMessageLine(event);
|
||||||
|
if (fileLine) lines.push(fileLine);
|
||||||
|
if (messageLine) lines.push(messageLine);
|
||||||
|
appendReplayTestProgressHintLine(lines, event);
|
||||||
|
lines.push(...replayTestProgressFailureContextLines(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSuppressReplayTestCaseDetailLines(
|
||||||
|
event: ReplayTestCaseProgressEvent,
|
||||||
|
options: ReplayTestProgressFormatOptions,
|
||||||
|
): boolean {
|
||||||
|
return options.verbose === true && event.status === 'fail';
|
||||||
|
}
|
||||||
|
|
||||||
|
function replayTestProgressFailureFileLine(event: ReplayTestCaseProgressEvent): string | undefined {
|
||||||
|
return event.status === 'fail' && event.title?.trim()
|
||||||
|
? ` file: ${path.basename(event.file)}`
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replayTestProgressMessageLine(event: ReplayTestCaseProgressEvent): string | undefined {
|
||||||
const message = event.message?.replace(/\s+/g, ' ').trim();
|
const message = event.message?.replace(/\s+/g, ' ').trim();
|
||||||
if (message) {
|
if (!message) return undefined;
|
||||||
lines.push(` ${event.status === 'fail' ? `failed at: ${message}` : message}`);
|
return ` ${event.status === 'fail' ? `failed at: ${message}` : message}`;
|
||||||
}
|
}
|
||||||
if (event.status === 'fail' && !event.retrying) {
|
|
||||||
if (event.session) lines.push(` session: ${event.session}`);
|
function appendReplayTestProgressHintLine(
|
||||||
if (event.artifactsDir) lines.push(` artifacts: ${event.artifactsDir}`);
|
lines: string[],
|
||||||
}
|
event: ReplayTestCaseProgressEvent,
|
||||||
|
): void {
|
||||||
|
const hint = event.hint?.replace(/\s+/g, ' ').trim();
|
||||||
|
if (event.status === 'fail' && hint) lines.push(` hint: ${hint}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replayTestProgressFailureContextLines(event: ReplayTestCaseProgressEvent): string[] {
|
||||||
|
if (event.status !== 'fail' || event.retrying) return [];
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (event.session) lines.push(` session: ${event.session}`);
|
||||||
|
if (event.artifactsDir) lines.push(` artifacts: ${event.artifactsDir}`);
|
||||||
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatReplayTestCaseSummaryLine(event: ReplayTestCaseProgressEvent): string {
|
function formatReplayTestCaseSummaryLine(event: ReplayTestCaseProgressEvent): string {
|
||||||
|
const useColor = supportsColor(process.stderr);
|
||||||
const statusLabel = formatReplayTestProgressStatusLabel(event);
|
const statusLabel = formatReplayTestProgressStatusLabel(event);
|
||||||
const name = formatReplayTestProgressName(event);
|
const name = formatReplayTestProgressName(event);
|
||||||
const shardSuffix = formatReplayTestProgressShardSuffix(event);
|
const shardSuffix = formatReplayTestProgressShardSuffix(event, { useColor });
|
||||||
const durationSuffix =
|
const durationSuffix =
|
||||||
event.durationMs !== undefined ? ` (${formatReplayProgressDuration(event)})` : '';
|
event.durationMs !== undefined ? ` ${formatReplayProgressDuration(event, { useColor })}` : '';
|
||||||
return `${statusLabel} ${name}${shardSuffix}${durationSuffix}`;
|
return `${statusLabel} ${name}${shardSuffix}${durationSuffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatReplayTestProgressName(event: ReplayTestCaseProgressEvent): string {
|
function formatReplayTestProgressName(event: ReplayTestCaseProgressEvent): string {
|
||||||
const title = event.title?.trim();
|
const title = event.title?.trim();
|
||||||
const file = path.basename(event.file);
|
const file = path.basename(event.file);
|
||||||
return title ? `${JSON.stringify(title)} in ${file}` : file;
|
return title ? title : file;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatReplayTestProgressStatusLabel(event: ReplayTestCaseProgressEvent): string {
|
function formatReplayTestProgressStatusLabel(event: ReplayTestCaseProgressEvent): string {
|
||||||
@@ -151,15 +195,30 @@ function colorizeProgressMarker(text: string, format: Parameters<typeof colorize
|
|||||||
return colorize(text, format, { validateStream: false });
|
return colorize(text, format, { validateStream: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatReplayTestProgressShardSuffix(event: ReplayTestCaseProgressEvent): string {
|
function formatReplayTestProgressShardSuffix(
|
||||||
|
event: ReplayTestCaseProgressEvent,
|
||||||
|
options: { useColor?: boolean } = {},
|
||||||
|
): string {
|
||||||
if (typeof event.shardIndex !== 'number') return '';
|
if (typeof event.shardIndex !== 'number') return '';
|
||||||
const shardCount = typeof event.shardCount === 'number' ? event.shardCount : '?';
|
const shardCount = typeof event.shardCount === 'number' ? event.shardCount : '?';
|
||||||
const device = typeof event.deviceId === 'string' ? ` ${event.deviceId}` : '';
|
const device = replayTestProgressShardDeviceName(event);
|
||||||
return ` [shard ${event.shardIndex + 1}/${shardCount}${device}]`;
|
const suffix = ` [${event.shardIndex + 1}/${shardCount}${device ? ` ${device}` : ''}]`;
|
||||||
|
return options.useColor ? colorizeProgressMarker(suffix, 'dim') : suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatReplayProgressDuration(event: ReplayTestCaseProgressEvent): string {
|
function replayTestProgressShardDeviceName(event: ReplayTestCaseProgressEvent): string | undefined {
|
||||||
return formatDurationSeconds(event.durationMs ?? 0);
|
const name = event.deviceName?.trim();
|
||||||
|
if (name) return name;
|
||||||
|
const id = event.deviceId?.trim();
|
||||||
|
return id || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatReplayProgressDuration(
|
||||||
|
event: ReplayTestCaseProgressEvent,
|
||||||
|
options: { useColor?: boolean } = {},
|
||||||
|
): string {
|
||||||
|
const duration = formatDurationSeconds(event.durationMs ?? 0);
|
||||||
|
return options.useColor ? colorizeProgressMarker(duration, 'yellow') : duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isReplayTestCompletionProgressEvent(event: ReplayTestCaseProgressEvent): boolean {
|
function isReplayTestCompletionProgressEvent(event: ReplayTestCaseProgressEvent): boolean {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
replayErrorHintLine,
|
replayErrorHintLine,
|
||||||
replayErrorLogLine,
|
replayErrorLogLine,
|
||||||
replayTestDisplayNameWithFile,
|
replayTestDisplayNameWithFile,
|
||||||
|
replayTestFailureFileLine,
|
||||||
type FailedReplayTestResult,
|
type FailedReplayTestResult,
|
||||||
type PassedReplayTestResult,
|
type PassedReplayTestResult,
|
||||||
} from './format.ts';
|
} from './format.ts';
|
||||||
@@ -37,9 +38,29 @@ function renderReplayTestSummary(
|
|||||||
|
|
||||||
function formatReplayTestSummaryLine(data: ReplaySuiteResult, flakyCount: number): string {
|
function formatReplayTestSummaryLine(data: ReplaySuiteResult, flakyCount: number): string {
|
||||||
const durationMs = typeof data.durationMs === 'number' ? data.durationMs : undefined;
|
const durationMs = typeof data.durationMs === 'number' ? data.durationMs : undefined;
|
||||||
|
const useColor = supportsColor();
|
||||||
|
const passed = formatReplaySummaryPassed(data.passed, { useColor });
|
||||||
|
const total = formatReplaySummaryTotal(data.total, { useColor });
|
||||||
|
const failedSuffix = data.failed > 0 ? `, ${data.failed} failed` : '';
|
||||||
const flakySuffix = flakyCount > 0 ? `, ${flakyCount} flaky` : '';
|
const flakySuffix = flakyCount > 0 ? `, ${flakyCount} flaky` : '';
|
||||||
const durationSuffix = durationMs !== undefined ? ` in ${formatDurationSeconds(durationMs)}` : '';
|
const durationSuffix =
|
||||||
return `Test summary: ${data.passed} passed, ${data.failed} failed${flakySuffix}${durationSuffix}`;
|
durationMs !== undefined ? ` in ${formatReplayDuration(durationMs, { useColor })}` : '';
|
||||||
|
return `Test summary: ${passed} ${total}${failedSuffix}${flakySuffix}${durationSuffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatReplaySummaryPassed(passed: number, options: { useColor?: boolean } = {}): string {
|
||||||
|
const text = `${passed} passed`;
|
||||||
|
return options.useColor ? colorize(text, 'green') : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatReplaySummaryTotal(total: number, options: { useColor?: boolean } = {}): string {
|
||||||
|
const text = `(${total})`;
|
||||||
|
return options.useColor ? colorize(text, 'dim') : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatReplayDuration(durationMs: number, options: { useColor?: boolean } = {}): string {
|
||||||
|
const duration = formatDurationSeconds(durationMs);
|
||||||
|
return options.useColor ? colorize(duration, 'yellow') : duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
function replayFlakyStatusIcon(): string {
|
function replayFlakyStatusIcon(): string {
|
||||||
@@ -97,6 +118,8 @@ function renderReplayFailureBody(
|
|||||||
context: ReplayTestReporterContext,
|
context: ReplayTestReporterContext,
|
||||||
indent: string,
|
indent: string,
|
||||||
): void {
|
): void {
|
||||||
|
const fileLine = replayTestFailureFileLine(result);
|
||||||
|
if (fileLine) context.writeStdout(`${indent}${fileLine}\n`);
|
||||||
context.writeStdout(`${indent}${result.error?.message ?? 'Unknown test failure'}\n`);
|
context.writeStdout(`${indent}${result.error?.message ?? 'Unknown test failure'}\n`);
|
||||||
for (const line of replayFailureConsoleLines(result)) {
|
for (const line of replayFailureConsoleLines(result)) {
|
||||||
context.writeStdout(`${indent}${line}\n`);
|
context.writeStdout(`${indent}${line}\n`);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function isFlakyReplayTestResult(
|
|||||||
export function replayTestDisplayNameWithFile(result: ReplaySuiteTestResult): string {
|
export function replayTestDisplayNameWithFile(result: ReplaySuiteTestResult): string {
|
||||||
const title = replayTestTitle(result);
|
const title = replayTestTitle(result);
|
||||||
const filename = path.basename(result.file);
|
const filename = path.basename(result.file);
|
||||||
const base = title && title.length > 0 ? `${JSON.stringify(title)} in ${filename}` : filename;
|
const base = title && title.length > 0 ? title : filename;
|
||||||
return `${base}${formatReplayTestShardSuffix(result)}`;
|
return `${base}${formatReplayTestShardSuffix(result)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +41,10 @@ export function replayArtifactsLine(
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function replayTestFailureFileLine(result: FailedReplayTestResult): string | undefined {
|
||||||
|
return replayTestTitle(result) ? `file: ${path.basename(result.file)}` : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function replayErrorHintLine(error: ReplayTestError): string | undefined {
|
export function replayErrorHintLine(error: ReplayTestError): string | undefined {
|
||||||
return error.hint ? `hint: ${error.hint}` : undefined;
|
return error.hint ? `hint: ${error.hint}` : undefined;
|
||||||
}
|
}
|
||||||
@@ -96,6 +100,10 @@ export function appendReplayTestShardMetadata(
|
|||||||
lines,
|
lines,
|
||||||
typeof result.deviceId === 'string' ? `deviceId: ${result.deviceId}` : undefined,
|
typeof result.deviceId === 'string' ? `deviceId: ${result.deviceId}` : undefined,
|
||||||
);
|
);
|
||||||
|
appendOptionalLine(
|
||||||
|
lines,
|
||||||
|
typeof result.deviceName === 'string' ? `deviceName: ${result.deviceName}` : undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replayTestWarningLines(result: ReplaySuiteTestResult): string[] {
|
export function replayTestWarningLines(result: ReplaySuiteTestResult): string[] {
|
||||||
@@ -132,6 +140,13 @@ function replayTestTitle(result: ReplaySuiteTestResult): string | undefined {
|
|||||||
export function formatReplayTestShardSuffix(result: ReplaySuiteTestResult): string {
|
export function formatReplayTestShardSuffix(result: ReplaySuiteTestResult): string {
|
||||||
if (!('shardIndex' in result) || typeof result.shardIndex !== 'number') return '';
|
if (!('shardIndex' in result) || typeof result.shardIndex !== 'number') return '';
|
||||||
const shardCount = typeof result.shardCount === 'number' ? result.shardCount : '?';
|
const shardCount = typeof result.shardCount === 'number' ? result.shardCount : '?';
|
||||||
const device = typeof result.deviceId === 'string' ? ` ${result.deviceId}` : '';
|
const device = replayTestShardDeviceName(result);
|
||||||
return ` [shard ${result.shardIndex + 1}/${shardCount}${device}]`;
|
return ` [${result.shardIndex + 1}/${shardCount}${device ? ` ${device}` : ''}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replayTestShardDeviceName(result: ReplaySuiteTestResult): string | undefined {
|
||||||
|
const name = 'deviceName' in result ? result.deviceName?.trim() : undefined;
|
||||||
|
if (name) return name;
|
||||||
|
const id = 'deviceId' in result ? result.deviceId?.trim() : undefined;
|
||||||
|
return id || undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const prepareCliSchema = {
|
|||||||
usageOverride: 'prepare ios-runner --platform ios|macos [--timeout <ms>]',
|
usageOverride: 'prepare ios-runner --platform ios|macos [--timeout <ms>]',
|
||||||
listUsageOverride: 'prepare',
|
listUsageOverride: 'prepare',
|
||||||
helpDescription:
|
helpDescription:
|
||||||
'Prepare platform helper infrastructure. ios-runner builds/reuses, starts, and health-checks the XCTest runner so later Apple snapshots and interactions do not pay first-use startup cost. In CI, run it after boot/install and before replay/test; if replay/test starts a separate daemon, run clean:daemon after prepare to release the prepared runner lease. It is not a recovery step for "runner already owned by another agent-device daemon"; stop or clean the owning daemon on the Mac with simulator access instead. Runner build/start output is written to the session runner.log; daemon.log is for daemon lifecycle/startup issues.',
|
'Prepare platform helper infrastructure. ios-runner builds/reuses, starts, and health-checks the XCTest runner so later Apple snapshots and interactions do not pay first-use startup cost. In CI, run it after boot/install and before replay/test; if replay/test starts a separate daemon, stop the prepare daemon before replay/test so it does not keep the prepared runner lease. It is not a recovery step for "runner already owned by another agent-device daemon"; stop the owning daemon on the Mac with simulator access instead. Runner build/start output is written to the session runner.log; daemon.log is for daemon lifecycle/startup issues.',
|
||||||
summary:
|
summary:
|
||||||
'Pre-warm platform helpers, especially the iOS/macOS XCTest runner before Apple automation',
|
'Pre-warm platform helpers, especially the iOS/macOS XCTest runner before Apple automation',
|
||||||
positionalArgs: ['ios-runner'],
|
positionalArgs: ['ios-runner'],
|
||||||
|
|||||||
@@ -740,7 +740,9 @@ test('parseMaestroReplayFlow parses the test-app Maestro suite fixture', () => {
|
|||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
parsed.actions.map((entry) => entry.command),
|
parsed.actions.map((entry) => entry.command),
|
||||||
[
|
[
|
||||||
|
'open',
|
||||||
'__maestroAssertVisible',
|
'__maestroAssertVisible',
|
||||||
|
'__maestroScrollUntilVisible',
|
||||||
'__maestroTapOn',
|
'__maestroTapOn',
|
||||||
'__maestroAssertVisible',
|
'__maestroAssertVisible',
|
||||||
'__maestroTapOn',
|
'__maestroTapOn',
|
||||||
@@ -760,4 +762,5 @@ test('parseMaestroReplayFlow parses the test-app Maestro suite fixture', () => {
|
|||||||
'__maestroAssertVisible',
|
'__maestroAssertVisible',
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
assert.equal(parsed.actions[0]?.flags.clearAppState, true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -509,6 +509,12 @@ test('test --shard-all runs each runnable entry on each selected device', async
|
|||||||
'emulator-5556',
|
'emulator-5556',
|
||||||
'emulator-5556',
|
'emulator-5556',
|
||||||
]);
|
]);
|
||||||
|
expect(tests.map((entry) => entry.deviceName)).toEqual([
|
||||||
|
'Pixel 8',
|
||||||
|
'Pixel 8',
|
||||||
|
'Pixel 8 Pro',
|
||||||
|
'Pixel 8 Pro',
|
||||||
|
]);
|
||||||
expect(tests.map((entry) => entry.artifactsDir)).toEqual([
|
expect(tests.map((entry) => entry.artifactsDir)).toEqual([
|
||||||
expect.stringContaining(`${path.sep}shard-1${path.sep}01-login`),
|
expect.stringContaining(`${path.sep}shard-1${path.sep}01-login`),
|
||||||
expect.stringContaining(`${path.sep}shard-1${path.sep}02-pay`),
|
expect.stringContaining(`${path.sep}shard-1${path.sep}02-pay`),
|
||||||
@@ -567,6 +573,9 @@ test('test --shard-split distributes runnable entries by modulo and keeps skips
|
|||||||
expect(tests.filter((entry) => entry.status === 'passed').map((entry) => entry.deviceId)).toEqual(
|
expect(tests.filter((entry) => entry.status === 'passed').map((entry) => entry.deviceId)).toEqual(
|
||||||
['emulator-5554', 'emulator-5554', 'emulator-5556'],
|
['emulator-5554', 'emulator-5554', 'emulator-5556'],
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
tests.filter((entry) => entry.status === 'passed').map((entry) => entry.deviceName),
|
||||||
|
).toEqual(['Pixel 8', 'Pixel 8', 'Pixel 8 Pro']);
|
||||||
expect(invoked.map((req) => req.flags?.serial).sort()).toEqual([
|
expect(invoked.map((req) => req.flags?.serial).sort()).toEqual([
|
||||||
'emulator-5554',
|
'emulator-5554',
|
||||||
'emulator-5554',
|
'emulator-5554',
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ function emitReplayTestRetryProgress(
|
|||||||
durationMs: attempt.durationMs,
|
durationMs: attempt.durationMs,
|
||||||
retrying: true,
|
retrying: true,
|
||||||
message: attempt.response.error.message,
|
message: attempt.response.error.message,
|
||||||
|
hint: attempt.response.error.hint,
|
||||||
session: attempt.sessionName,
|
session: attempt.sessionName,
|
||||||
artifactsDir: context.testArtifactsDir,
|
artifactsDir: context.testArtifactsDir,
|
||||||
...replayTestProgressShardMetadata(params.shard),
|
...replayTestProgressShardMetadata(params.shard),
|
||||||
@@ -322,6 +323,7 @@ function buildReplayTestFailedResult(
|
|||||||
session: outcome.finalSessionName,
|
session: outcome.finalSessionName,
|
||||||
artifactsDir: context.testArtifactsDir,
|
artifactsDir: context.testArtifactsDir,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
hint: error.hint,
|
||||||
...replayTestProgressShardMetadata(shard),
|
...replayTestProgressShardMetadata(shard),
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -379,18 +381,19 @@ function readReplayResponseSnapshotDiagnostics(response: DaemonResponse | undefi
|
|||||||
|
|
||||||
function replayTestShardResultMetadata(
|
function replayTestShardResultMetadata(
|
||||||
shard: ReplayTestShardContext | undefined,
|
shard: ReplayTestShardContext | undefined,
|
||||||
): Pick<ReplaySuiteTestFailed, 'shardIndex' | 'shardCount' | 'deviceId'> {
|
): Pick<ReplaySuiteTestFailed, 'shardIndex' | 'shardCount' | 'deviceId' | 'deviceName'> {
|
||||||
return replayTestProgressShardMetadata(shard);
|
return replayTestProgressShardMetadata(shard);
|
||||||
}
|
}
|
||||||
|
|
||||||
function replayTestProgressShardMetadata(
|
function replayTestProgressShardMetadata(
|
||||||
shard: ReplayTestShardContext | undefined,
|
shard: ReplayTestShardContext | undefined,
|
||||||
): Pick<ReplaySuiteTestFailed, 'shardIndex' | 'shardCount' | 'deviceId'> {
|
): Pick<ReplaySuiteTestFailed, 'shardIndex' | 'shardCount' | 'deviceId' | 'deviceName'> {
|
||||||
return shard
|
return shard
|
||||||
? {
|
? {
|
||||||
shardIndex: shard.shardIndex,
|
shardIndex: shard.shardIndex,
|
||||||
shardCount: shard.shardCount,
|
shardCount: shard.shardCount,
|
||||||
deviceId: shard.device.id,
|
deviceId: shard.device.id,
|
||||||
|
deviceName: shard.device.name,
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ function buildUnexpectedShardFailure(
|
|||||||
shardIndex: shard.shardIndex,
|
shardIndex: shard.shardIndex,
|
||||||
shardCount: shard.shardCount,
|
shardCount: shard.shardCount,
|
||||||
deviceId: shard.device.id,
|
deviceId: shard.device.id,
|
||||||
|
deviceName: shard.device.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,11 +25,13 @@ export type ReplayTestProgressEvent = {
|
|||||||
durationMs?: number;
|
durationMs?: number;
|
||||||
retrying?: boolean;
|
retrying?: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
hint?: string;
|
||||||
session?: string;
|
session?: string;
|
||||||
artifactsDir?: string;
|
artifactsDir?: string;
|
||||||
shardIndex?: number;
|
shardIndex?: number;
|
||||||
shardCount?: number;
|
shardCount?: number;
|
||||||
deviceId?: string;
|
deviceId?: string;
|
||||||
|
deviceName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CommandProgressEvent = {
|
export type CommandProgressEvent = {
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export type ReplaySuiteTestPassed = {
|
|||||||
shardIndex?: number;
|
shardIndex?: number;
|
||||||
shardCount?: number;
|
shardCount?: number;
|
||||||
deviceId?: string;
|
deviceId?: string;
|
||||||
|
deviceName?: string;
|
||||||
snapshotDiagnostics?: SnapshotDiagnosticsSummary;
|
snapshotDiagnostics?: SnapshotDiagnosticsSummary;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ export type ReplaySuiteTestFailed = {
|
|||||||
shardIndex?: number;
|
shardIndex?: number;
|
||||||
shardCount?: number;
|
shardCount?: number;
|
||||||
deviceId?: string;
|
deviceId?: string;
|
||||||
|
deviceName?: string;
|
||||||
snapshotDiagnostics?: SnapshotDiagnosticsSummary;
|
snapshotDiagnostics?: SnapshotDiagnosticsSummary;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -772,10 +772,26 @@ test('runner session startup rejects live foreign runner lease', async () => {
|
|||||||
String((thrown as { details?: Record<string, unknown> }).details?.hint),
|
String((thrown as { details?: Record<string, unknown> }).details?.hint),
|
||||||
/Do not run prepare ios-runner/,
|
/Do not run prepare ios-runner/,
|
||||||
);
|
);
|
||||||
|
assert.match(
|
||||||
|
String((thrown as { details?: Record<string, unknown> }).details?.hint),
|
||||||
|
/^If it is stuck, stop the owning agent-device daemon for AGENT_DEVICE_STATE_DIR='\/tmp\/agent-device-owner' and retry/,
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
String((thrown as { details?: Record<string, unknown> }).details?.hint),
|
||||||
|
/pnpm|clean:daemon/,
|
||||||
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
String((thrown as { details?: Record<string, unknown> }).details?.hint),
|
String((thrown as { details?: Record<string, unknown> }).details?.hint),
|
||||||
/PID \d+ with AGENT_DEVICE_STATE_DIR=\/tmp\/agent-device-owner/,
|
/PID \d+ with AGENT_DEVICE_STATE_DIR=\/tmp\/agent-device-owner/,
|
||||||
);
|
);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
String((thrown as { details?: Record<string, unknown> }).details?.hint),
|
||||||
|
/AGENT_DEVICE_STATE_DIR=\/tmp\/agent-device-owner\./,
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
String((thrown as { details?: Record<string, unknown> }).details?.hint),
|
||||||
|
/Current daemon state dir/,
|
||||||
|
);
|
||||||
assert.equal(mockRunCmdBackground.mock.calls.length, 0);
|
assert.equal(mockRunCmdBackground.mock.calls.length, 0);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
mockRunAppleToolCommand.mock.calls.some((call) => call[0] === 'pkill'),
|
mockRunAppleToolCommand.mock.calls.some((call) => call[0] === 'pkill'),
|
||||||
@@ -825,10 +841,19 @@ test('runner session busy error includes logical lease context after admission',
|
|||||||
deviceKey: device.id,
|
deviceKey: device.id,
|
||||||
});
|
});
|
||||||
assert.match(String(thrown.details?.hint), /five-minute inactivity lease expires/);
|
assert.match(String(thrown.details?.hint), /five-minute inactivity lease expires/);
|
||||||
|
assert.match(
|
||||||
|
String(thrown.details?.hint),
|
||||||
|
/^If it is stuck, stop the owning agent-device daemon for AGENT_DEVICE_STATE_DIR='\/tmp\/agent-device-owner' and retry/,
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(String(thrown.details?.hint), /pnpm|clean:daemon/);
|
||||||
assert.match(
|
assert.match(
|
||||||
String(thrown.details?.hint),
|
String(thrown.details?.hint),
|
||||||
/Runner owner: PID \d+ with AGENT_DEVICE_STATE_DIR=\/tmp\/agent-device-owner/,
|
/Runner owner: PID \d+ with AGENT_DEVICE_STATE_DIR=\/tmp\/agent-device-owner/,
|
||||||
);
|
);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
String(thrown.details?.hint),
|
||||||
|
/AGENT_DEVICE_STATE_DIR=\/tmp\/agent-device-owner\./,
|
||||||
|
);
|
||||||
assert.equal(mockRunCmdBackground.mock.calls.length, 0);
|
assert.equal(mockRunCmdBackground.mock.calls.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -188,26 +188,44 @@ function buildBusyRunnerLeaseHint(
|
|||||||
lease: RunnerLease,
|
lease: RunnerLease,
|
||||||
logicalLeaseContext?: RunnerLogicalLeaseContext,
|
logicalLeaseContext?: RunnerLogicalLeaseContext,
|
||||||
): string {
|
): string {
|
||||||
const owner = `PID ${lease.ownerPid}`;
|
const owner = buildRunnerOwnerHint(lease);
|
||||||
const stateDir = lease.ownerStateDir ? ` with AGENT_DEVICE_STATE_DIR=${lease.ownerStateDir}` : '';
|
const cleanup = buildBusyRunnerLeaseCleanupHint(lease);
|
||||||
const currentStateDir = readCurrentStateDir();
|
|
||||||
const current =
|
|
||||||
currentStateDir && currentStateDir !== lease.ownerStateDir
|
|
||||||
? ` Current daemon state dir is ${currentStateDir}.`
|
|
||||||
: '';
|
|
||||||
if (logicalLeaseContext) {
|
if (logicalLeaseContext) {
|
||||||
return [
|
return [
|
||||||
`The device is busy because another active device lease owns it, or the runner is owned by another daemon/process after lease admission. Runner owner: ${owner}${stateDir}.${current}`,
|
cleanup,
|
||||||
|
owner,
|
||||||
|
'The device is busy because another active device lease owns it, or the runner is owned by another daemon/process after lease admission.',
|
||||||
'Retry after the owning session closes or after the five-minute inactivity lease expires.',
|
'Retry after the owning session closes or after the five-minute inactivity lease expires.',
|
||||||
'If this persists after expiry, inspect the runner owner details and clean the stale daemon state on the machine with simulator access.',
|
|
||||||
].join(' ');
|
].join(' ');
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
`Runner owner details: ${owner}${stateDir}.${current} Retry after the owning runner finishes.`,
|
cleanup,
|
||||||
'Do not run prepare ios-runner from another daemon/client to recover this; a live foreign runner lease cannot be released by the remote client.',
|
owner,
|
||||||
|
'If the runner is still active, wait for it to finish. Do not run prepare ios-runner from another daemon/client to recover this.',
|
||||||
].join(' ');
|
].join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRunnerOwnerHint(lease: RunnerLease): string {
|
||||||
|
const owner = `Runner owner: PID ${lease.ownerPid}`;
|
||||||
|
if (lease.ownerStateDir) return `${owner} with AGENT_DEVICE_STATE_DIR=${lease.ownerStateDir}`;
|
||||||
|
return `${owner}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBusyRunnerLeaseCleanupHint(lease: RunnerLease): string {
|
||||||
|
if (lease.ownerStateDir) {
|
||||||
|
return `If it is stuck, stop the owning agent-device daemon for ${formatEnvAssignment('AGENT_DEVICE_STATE_DIR', lease.ownerStateDir)} and retry.`;
|
||||||
|
}
|
||||||
|
return 'If it is stuck, stop the owning agent-device daemon and retry.';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEnvAssignment(name: string, value: string): string {
|
||||||
|
return `${name}=${shellQuote(value)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
export async function cleanupOwnedRunnerLease(
|
export async function cleanupOwnedRunnerLease(
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
cleanup: RunnerLeaseCleanupAdapter,
|
cleanup: RunnerLeaseCleanupAdapter,
|
||||||
|
|||||||
@@ -1391,7 +1391,8 @@ test('usageForCommand documents prepare ios-runner', () => {
|
|||||||
assert.match(help, /--timeout <ms>/);
|
assert.match(help, /--timeout <ms>/);
|
||||||
assert.match(help, /XCTest runner/);
|
assert.match(help, /XCTest runner/);
|
||||||
assert.match(help, /separate daemon/);
|
assert.match(help, /separate daemon/);
|
||||||
assert.match(help, /clean:daemon after prepare/);
|
assert.match(help, /stop the prepare daemon before replay\/test/);
|
||||||
|
assert.doesNotMatch(help, /clean:daemon|pnpm/);
|
||||||
assert.match(
|
assert.match(
|
||||||
help,
|
help,
|
||||||
/not a recovery step for "runner already owned by another agent-device daemon"/,
|
/not a recovery step for "runner already owned by another agent-device daemon"/,
|
||||||
|
|||||||
@@ -558,8 +558,8 @@ test('sendToDaemon prints replay test progress before the socket response', asyn
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(response, { ok: true, data: { via: 'socket' } });
|
assert.deepEqual(response, { ok: true, data: { via: 'socket' } });
|
||||||
assert.match(stderr, /✓ "Login flow" in 01-login\.ad \(1\.23s\)/);
|
assert.match(stderr, /✓ Login flow 1\.23s/);
|
||||||
assert.equal(stderr.match(/✓ "Login flow" in 01-login\.ad \(1\.23s\)/g)?.length, 1);
|
assert.equal(stderr.match(/✓ Login flow 1\.23s/g)?.length, 1);
|
||||||
assert.match(stderr, /steps \(attempt 2\):/);
|
assert.match(stderr, /steps \(attempt 2\):/);
|
||||||
assert.match(stderr, /open "Demo" \(line 3, 0\.25s\)/);
|
assert.match(stderr, /open "Demo" \(line 3, 0\.25s\)/);
|
||||||
assert.match(stderr, /assertVisible "text=\\"Home\\"" "3000" \(line 4, 0\.75s\)/);
|
assert.match(stderr, /assertVisible "text=\\"Home\\"" "3000" \(line 4, 0\.75s\)/);
|
||||||
@@ -663,7 +663,7 @@ test('sendToDaemon prints replay test progress before the HTTP NDJSON response',
|
|||||||
assert.deepEqual(response, { ok: true, data: { via: 'http-progress' } });
|
assert.deepEqual(response, { ok: true, data: { via: 'http-progress' } });
|
||||||
});
|
});
|
||||||
assert.deepEqual(seenPaths, ['GET /agent-device/health', 'POST /agent-device/rpc']);
|
assert.deepEqual(seenPaths, ['GET /agent-device/health', 'POST /agent-device/rpc']);
|
||||||
assert.match(stderr, /✓ "Payments flow" in 02-payments\.ad \(2\.50s\)/);
|
assert.match(stderr, /✓ Payments flow 2\.50s/);
|
||||||
} finally {
|
} finally {
|
||||||
(http as unknown as { request: typeof http.request }).request = originalHttpRequest;
|
(http as unknown as { request: typeof http.request }).request = originalHttpRequest;
|
||||||
process.stderr.write = originalStderrWrite;
|
process.stderr.write = originalStderrWrite;
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ Bootstrap:
|
|||||||
agent-device prepare ios-runner --platform ios --timeout 240000
|
agent-device prepare ios-runner --platform ios --timeout 240000
|
||||||
If app id is unknown, plan devices, apps, then open <discovered-app-id>. Discovery is not enough when the task asks to open/start the app.
|
If app id is unknown, plan devices, apps, then open <discovered-app-id>. Discovery is not enough when the task asks to open/start the app.
|
||||||
Install arguments are app/package id then artifact path. If the task says install, use install; use reinstall only when explicitly requested. Fresh runtime state is open --relaunch after install.
|
Install arguments are app/package id then artifact path. If the task says install, use install; use reinstall only when explicitly requested. Fresh runtime state is open --relaunch after install.
|
||||||
In Apple CI, run prepare ios-runner after boot/install and before replay/test. prepare ios-runner builds/reuses the XCTest runner, health-checks it with a lightweight command, and retries one stuck/non-connecting runner launch before the first snapshot pays that setup cost. It is not a recovery step for "runner already owned by another agent-device daemon"; stop or clean the owning daemon on the Mac with simulator access instead. If the replay/test step starts a separate daemon, run clean:daemon after prepare so the prepared runner does not keep a live lease owned by the prepare daemon.
|
In Apple CI, run prepare ios-runner after boot/install and before replay/test. prepare ios-runner builds/reuses the XCTest runner, health-checks it with a lightweight command, and retries one stuck/non-connecting runner launch before the first snapshot pays that setup cost. It is not a recovery step for "runner already owned by another agent-device daemon"; stop the owning daemon on the Mac with simulator access instead. If the replay/test step starts a separate daemon, stop the prepare daemon before replay/test so the prepared runner does not keep a live lease owned by that daemon.
|
||||||
CI may cache ~/.agent-device/ios-runner/derived with an exact key that includes the agent-device package and Xcode version. Avoid broad restore-key fallbacks; prepare ios-runner already recovers bad restored runner artifacts and one retryable non-connecting runner launch. Runner build/start output is written to the session's runner.log; daemon.log is for daemon lifecycle/startup issues.
|
CI may cache ~/.agent-device/ios-runner/derived with an exact key that includes the agent-device package and Xcode version. Avoid broad restore-key fallbacks; prepare ios-runner already recovers bad restored runner artifacts and one retryable non-connecting runner launch. Runner build/start output is written to the session's runner.log; daemon.log is for daemon lifecycle/startup issues.
|
||||||
Do not open artifact paths or invent package ids. If apps lookup misses the target and no URL/artifact is provided, ask or stop.
|
Do not open artifact paths or invent package ids. If apps lookup misses the target and no URL/artifact is provided, ask or stop.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user