@browserstack/mcp-server 2.3.0-beta.4 → 2.3.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/capability/devicecontrol.capability-index.json +153 -18
- package/capability/mobiledevicecontrol.capability-index.json +369 -52
- package/dist/tools/applive.js +10 -1
- package/dist/tools/capability-registry/fallback.d.ts +40 -0
- package/dist/tools/capability-registry/fallback.js +61 -0
- package/dist/tools/capability-registry/index-loader.js +46 -4
- package/dist/tools/capability-registry/register.js +21 -11
- package/dist/tools/capability-registry/search.js +2 -1
- package/dist/tools/capability-registry/types.d.ts +64 -20
- package/dist/tools/capability-registry/types.js +13 -0
- package/dist/tools/failurelogs-utils/app-automate.js +1 -1
- package/dist/tools/live.js +8 -1
- package/package.json +1 -1
|
@@ -76,13 +76,17 @@
|
|
|
76
76
|
"Every session occupies a device and consumes App Automate minutes until it ends: always finish with set_session_status and then delete_session.",
|
|
77
77
|
"To let a person watch the device live while the agent drives it, set interactive_debugging true and leave video on; they then open the running session on the App Automate dashboard and start the interactive session there. That window is two-way: a person's taps and scrolls reach the same device, so ask them to watch only, or re-read the page source before every action.",
|
|
78
78
|
"device_name and platform_version must be a combination mobile device control offers; an unknown pair fails with a session-not-created error naming the capability.",
|
|
79
|
-
"Set appium_version to 2.19.0 for any session that will
|
|
79
|
+
"Set appium_version to 2.19.0 for any session that will scroll with scroll_gesture, dismiss an iOS keyboard with dismiss_keyboard_key, or use any other mobile: capability; the default server is 1.x and answers those with a 404 listing the commands it does know.",
|
|
80
80
|
"enable_biometric, enable_camera_image_injection and upload_media only take effect when set here \u2014 none of them can be turned on once the session is running.",
|
|
81
81
|
"Credentials belong in the Authorization header, never inside bstack:options: capabilities are echoed back in session-not-created errors.",
|
|
82
82
|
"device_name and platform_version come from list_devices; get_plan first if sessions are queueing rather than starting.",
|
|
83
83
|
"Treat a response without value.sessionId as a failed start even when the status is 200, and treat a failed call the same way: allocation can outlast the client's own timeout, so BrowserStack may have started a session the answer never named. One is then holding a device and consuming minutes until its idle timeout.",
|
|
84
84
|
"After a start that failed or returned no session id, look for a session left running on the account and end it rather than starting another.",
|
|
85
|
-
"The response carries no link of any kind \u2014 it is the session id and the negotiated capabilities only. Call get_session for the links: browser_url to watch the run on the dashboard, and public_url to share it with someone who has no BrowserStack account."
|
|
85
|
+
"The response carries no link of any kind \u2014 it is the session id and the negotiated capabilities only. Call get_session for the links: browser_url to watch the run on the dashboard, and public_url to share it with someone who has no BrowserStack account.",
|
|
86
|
+
"LANGUAGE AND LOCALE LAND IN DIFFERENT PLACES ON THE TWO PLATFORMS. On Android they change the device, and the app with it \u2014 measured with language de and locale DE: the system's own permission dialog came up in German and the app rendered German throughout. On iOS they leave the device alone \u2014 Settings and General stay in English \u2014 and apply to the app under test at launch. Either way nothing is visible unless the app actually ships that language, so test with an app that does and read the strings back from get_page_source rather than trusting the call's 200.",
|
|
87
|
+
"Both are accepted without complaint on both platforms, so a 200 says nothing about whether anything changed. There is also no way to confirm the device locale from inside the session: read_device_property refuses getprop persist.sys.locale and ro.product.locale as unsupported commands. Judge it from what is on screen.",
|
|
88
|
+
"THIS CALL RETURNS NO LINK \u2014 only value.sessionId. Nothing here can open a browser for the user either: this surface makes HTTP requests, and on a remote server there is no browser to open. So never say a live view was opened. If the user may want to watch, call get_session straight after this and give them browser_url in the same reply, which is the session on the dashboard and needs a BrowserStack login. public_url needs no login but carries an auth token, so offer it only when they ask to share outside the team.",
|
|
89
|
+
"Someone can only WATCH AND INTERACT if interactive_debugging was set true in this call \u2014 it cannot be turned on once the session is running, it is off by default on Android, and it needs video left on. Ask for it up front when a person is going to follow along, because the alternative is restarting the session."
|
|
86
90
|
],
|
|
87
91
|
"body": [
|
|
88
92
|
{
|
|
@@ -209,15 +213,15 @@
|
|
|
209
213
|
"name": "language",
|
|
210
214
|
"type": "string",
|
|
211
215
|
"json_path": "/capabilities/alwaysMatch/appium:language",
|
|
212
|
-
"description": "
|
|
213
|
-
"example": "
|
|
216
|
+
"description": "Language code applied at session start. On Android it sets the DEVICE language \u2014 the system's own dialogs render in it \u2014 and the app follows. On iOS it does NOT change the device: Settings stays in English, and the code is applied to the app under test at launch instead.",
|
|
217
|
+
"example": "de"
|
|
214
218
|
},
|
|
215
219
|
{
|
|
216
220
|
"name": "locale",
|
|
217
221
|
"type": "string",
|
|
218
222
|
"json_path": "/capabilities/alwaysMatch/appium:locale",
|
|
219
|
-
"description": "
|
|
220
|
-
"example": "
|
|
223
|
+
"description": "Region applied with `language`, same split: the device on Android, the app under test on iOS. Android takes a bare region code such as DE.",
|
|
224
|
+
"example": "DE"
|
|
221
225
|
},
|
|
222
226
|
{
|
|
223
227
|
"name": "timezone",
|
|
@@ -459,12 +463,12 @@
|
|
|
459
463
|
"entity": "app_session",
|
|
460
464
|
"intent": "Type text into an input field in the app \u2014 use this to fill a search box, login form or any editable field found with find_element.",
|
|
461
465
|
"guidance": [
|
|
462
|
-
"REPLACES whatever the field already holds
|
|
466
|
+
"WHETHER THIS REPLACES OR APPENDS DEPENDS ON THE PLATFORM: on Android it replaces whatever the field already holds, and on iOS it appends to it. Typing twice into one iOS field therefore leaves both values joined. Read the field back from get_page_source whenever the resulting value matters.",
|
|
463
467
|
"The element must be an editable field (EditText on Android, TextField on iOS); typing into anything else fails with \"invalid element state\".",
|
|
464
468
|
"The on-screen keyboard may stay open afterwards and cover lower elements \u2014 re-read the page source before the next tap.",
|
|
465
|
-
"
|
|
466
|
-
"The keyboard is usually left open afterwards
|
|
467
|
-
"Sending a newline
|
|
469
|
+
"clear_element empties a field, and on iOS \u2014 where this APPENDS \u2014 it is the only way to take back what is already there. It answers 200 whether or not the field changed, so read the field back from get_page_source rather than trusting the status.",
|
|
470
|
+
"The keyboard is usually left open afterwards. On Android and on an iPad, hide_keyboard closes it. On an iPhone it cannot be closed without submitting, so read hide_keyboard's guidance before planning a tap below the field.",
|
|
471
|
+
"Sending a newline closes the keyboard on iOS by pressing Return, which an app that commits on editing-ended will treat as a submit \u2014 the field resets and the value moves wherever the app sends it. The commit is the app's doing, not the newline's: dismissing the same field any other way can commit it too. See hide_keyboard."
|
|
468
472
|
],
|
|
469
473
|
"path_params": [
|
|
470
474
|
{
|
|
@@ -504,6 +508,51 @@
|
|
|
504
508
|
}
|
|
505
509
|
}
|
|
506
510
|
},
|
|
511
|
+
{
|
|
512
|
+
"name": "clear_element",
|
|
513
|
+
"method": "POST",
|
|
514
|
+
"path": "/wd/hub/session/{session_id}/element/{element_id}/clear",
|
|
515
|
+
"mode": "write",
|
|
516
|
+
"entity": "app_session",
|
|
517
|
+
"intent": "Empty an editable field in the app \u2014 use this before typing when the field already holds something and you need to replace rather than add to it.",
|
|
518
|
+
"guidance": [
|
|
519
|
+
"Takes no parameters of its own.",
|
|
520
|
+
"CONFIRM IT ACTUALLY EMPTIED THE FIELD by reading the field back from get_page_source: this endpoint can answer 200 without the field changing, and the status alone does not tell you which happened.",
|
|
521
|
+
"Needed far more on iOS than on Android: send_keys REPLACES the field's contents on Android, so clearing first is usually unnecessary there, while on iOS it APPENDS and this is the only way to take back what is already in the field.",
|
|
522
|
+
"The element must be an editable field \u2014 EditText on Android, TextField on iOS. Anything else answers \"invalid element state\".",
|
|
523
|
+
"The element_id must come from find_element on the current screen; after the screen changes it is stale and fails with \"stale element reference\"."
|
|
524
|
+
],
|
|
525
|
+
"path_params": [
|
|
526
|
+
{
|
|
527
|
+
"name": "session_id",
|
|
528
|
+
"type": "string",
|
|
529
|
+
"required": true,
|
|
530
|
+
"example": "0123456789abcdef0123456789abcdef01234567"
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
"name": "element_id",
|
|
534
|
+
"type": "string",
|
|
535
|
+
"required": true,
|
|
536
|
+
"description": "Element id from find_element.",
|
|
537
|
+
"example": "element-6066-11e4-a52e-4f735466cecf"
|
|
538
|
+
}
|
|
539
|
+
],
|
|
540
|
+
"returns": [
|
|
541
|
+
"value"
|
|
542
|
+
],
|
|
543
|
+
"responses": {
|
|
544
|
+
"200": {
|
|
545
|
+
"description": "Field emptied; value is null. The null says the call was accepted, not that the field changed \u2014 read the field back to know that.",
|
|
546
|
+
"schema": {
|
|
547
|
+
"$schema": "NullValueResponse"
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
"404": {
|
|
551
|
+
"$response": "WebDriverError"
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
"send_empty_body": true
|
|
555
|
+
},
|
|
507
556
|
{
|
|
508
557
|
"name": "perform_actions",
|
|
509
558
|
"method": "POST",
|
|
@@ -516,7 +565,7 @@
|
|
|
516
565
|
"Tap: one touch pointer with pointerMove(duration 0, x, y), pointerDown(button 0), pause(100), pointerUp(button 0).",
|
|
517
566
|
"Swipe up to scroll content down: pointerMove to a low point, pointerDown, pointerMove(duration 600) to a higher point, pointerUp. Keep the start and end inside the scrollable element's bounds.",
|
|
518
567
|
"Long-press: replace the pause duration with 1000 or more.",
|
|
519
|
-
"For a scroll without computing coordinates,
|
|
568
|
+
"For a scroll without computing coordinates, scroll_gesture is simpler on Android and reports whether more content remains; on iOS use scroll_view; compute a swipe here only when neither fits.",
|
|
520
569
|
"To go back on an iOS screen that ignores press_back, swipe in from the left edge: a pointer sequence starting at exactly x=0 and moving right in several short steps. A single long move does not register."
|
|
521
570
|
],
|
|
522
571
|
"path_params": [
|
|
@@ -607,6 +656,201 @@
|
|
|
607
656
|
}
|
|
608
657
|
}
|
|
609
658
|
},
|
|
659
|
+
{
|
|
660
|
+
"name": "scroll_gesture",
|
|
661
|
+
"method": "POST",
|
|
662
|
+
"path": "/wd/hub/session/{session_id}/execute/sync",
|
|
663
|
+
"mode": "write",
|
|
664
|
+
"entity": "app_session",
|
|
665
|
+
"intent": "Scroll a region of the screen in a direction \u2014 use this to move through a list or page without working out start and end coordinates yourself.",
|
|
666
|
+
"guidance": [
|
|
667
|
+
"ANDROID ONLY. On iOS this is a 405 \"unknown method\"; scroll_view is the iOS counterpart.",
|
|
668
|
+
"The returned value is true when the view can still scroll further in that direction and false when it cannot \u2014 but the FIRST scroll of a freshly opened list can answer false while the screen moved and more content followed, so treat false as a hint and confirm from the page source before deciding you are at the end.",
|
|
669
|
+
"The area is a rectangle in screen pixels \u2014 get_window_rect gives the screen size to base it on \u2014 and percent is how far to scroll within it, 0 to 1.",
|
|
670
|
+
"Needs Appium 2.x: set appium_version to 2.19.0 at session start, since the default 1.x server answers mobile: commands with a 404 listing what it does know."
|
|
671
|
+
],
|
|
672
|
+
"path_params": [
|
|
673
|
+
{
|
|
674
|
+
"name": "session_id",
|
|
675
|
+
"type": "string",
|
|
676
|
+
"required": true,
|
|
677
|
+
"example": "0123456789abcdef0123456789abcdef01234567"
|
|
678
|
+
}
|
|
679
|
+
],
|
|
680
|
+
"body": [
|
|
681
|
+
{
|
|
682
|
+
"name": "script",
|
|
683
|
+
"type": "string",
|
|
684
|
+
"required": true,
|
|
685
|
+
"example": "mobile: scrollGesture",
|
|
686
|
+
"values": [
|
|
687
|
+
"mobile: scrollGesture"
|
|
688
|
+
]
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
"name": "args",
|
|
692
|
+
"type": "array",
|
|
693
|
+
"required": true,
|
|
694
|
+
"description": "Exactly one object: the rectangle to scroll inside, the direction, and how far.",
|
|
695
|
+
"example": [
|
|
696
|
+
{
|
|
697
|
+
"left": 100,
|
|
698
|
+
"top": 500,
|
|
699
|
+
"width": 880,
|
|
700
|
+
"height": 1500,
|
|
701
|
+
"direction": "down",
|
|
702
|
+
"percent": 0.8
|
|
703
|
+
}
|
|
704
|
+
]
|
|
705
|
+
}
|
|
706
|
+
],
|
|
707
|
+
"returns": [
|
|
708
|
+
"value"
|
|
709
|
+
],
|
|
710
|
+
"responses": {
|
|
711
|
+
"200": {
|
|
712
|
+
"description": "Command result; its shape depends on the command.",
|
|
713
|
+
"schema": {
|
|
714
|
+
"$schema": "AnyValueResponse"
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
"404": {
|
|
718
|
+
"$response": "WebDriverError"
|
|
719
|
+
},
|
|
720
|
+
"500": {
|
|
721
|
+
"$response": "WebDriverError"
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
"name": "scroll_view",
|
|
727
|
+
"method": "POST",
|
|
728
|
+
"path": "/wd/hub/session/{session_id}/execute/sync",
|
|
729
|
+
"mode": "write",
|
|
730
|
+
"entity": "app_session",
|
|
731
|
+
"intent": "Scroll a scrollable view on iOS in a direction, or until a named element is visible \u2014 use this to move through a list or a long screen.",
|
|
732
|
+
"guidance": [
|
|
733
|
+
"iOS ONLY, and the counterpart of scroll_gesture. On Android the same command exists with a different shape and answers 400 \"invalid argument\" naming strategy and selector as missing; use scroll_gesture there.",
|
|
734
|
+
"Scroll by direction with {\"direction\":\"down\"}, or stop when something comes into view with {\"predicateString\":\"name == 'Submit'\", \"toVisible\": true} \u2014 the second is the reliable way to reach a known control without guessing how far to go.",
|
|
735
|
+
"Pass elementId in args to scroll inside that view rather than the whole screen; without one it scrolls the first scrollable view it finds, which may not be the one you meant.",
|
|
736
|
+
"Needs Appium 2.x: set appium_version to 2.19.0 at session start, since the default 1.x server answers mobile: commands with a 404 listing what it does know.",
|
|
737
|
+
"Confirm from get_page_source that the screen actually moved \u2014 this answers 200 even on a screen with nothing scrollable, so a 200 only says the gesture was delivered."
|
|
738
|
+
],
|
|
739
|
+
"path_params": [
|
|
740
|
+
{
|
|
741
|
+
"name": "session_id",
|
|
742
|
+
"type": "string",
|
|
743
|
+
"required": true,
|
|
744
|
+
"example": "0123456789abcdef0123456789abcdef01234567"
|
|
745
|
+
}
|
|
746
|
+
],
|
|
747
|
+
"body": [
|
|
748
|
+
{
|
|
749
|
+
"name": "script",
|
|
750
|
+
"type": "string",
|
|
751
|
+
"required": true,
|
|
752
|
+
"values": [
|
|
753
|
+
"mobile: scroll"
|
|
754
|
+
],
|
|
755
|
+
"example": "mobile: scroll"
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
"name": "args",
|
|
759
|
+
"type": "array",
|
|
760
|
+
"required": true,
|
|
761
|
+
"description": "Exactly one object: a direction, or a predicateString with toVisible, optionally with elementId naming the view to scroll inside.",
|
|
762
|
+
"example": [
|
|
763
|
+
{
|
|
764
|
+
"direction": "down"
|
|
765
|
+
}
|
|
766
|
+
]
|
|
767
|
+
}
|
|
768
|
+
],
|
|
769
|
+
"returns": [
|
|
770
|
+
"value"
|
|
771
|
+
],
|
|
772
|
+
"responses": {
|
|
773
|
+
"200": {
|
|
774
|
+
"description": "Command result; its shape depends on the command.",
|
|
775
|
+
"schema": {
|
|
776
|
+
"$schema": "AnyValueResponse"
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
"404": {
|
|
780
|
+
"$response": "WebDriverError"
|
|
781
|
+
},
|
|
782
|
+
"500": {
|
|
783
|
+
"$response": "WebDriverError"
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
"name": "dismiss_keyboard_key",
|
|
789
|
+
"method": "POST",
|
|
790
|
+
"path": "/wd/hub/session/{session_id}/execute/sync",
|
|
791
|
+
"mode": "write",
|
|
792
|
+
"entity": "app_session",
|
|
793
|
+
"intent": "Close the on-screen keyboard by pressing one of its own keys \u2014 use this on an iPhone, where the plain hide_keyboard is refused.",
|
|
794
|
+
"guidance": [
|
|
795
|
+
"hide_keyboard already presses these keys on an iPhone when its plain call is refused \u2014 Done-type keys first, then Return \u2014 and reports each attempt. Call this directly to press exactly the key you choose: Return alone, or every key but Return when a submit must not happen.",
|
|
796
|
+
"The key must EXIST on that keyboard or this returns the same 400: read the keyboard from get_page_source and pick from the keys that are really there. An iPad carries \"Hide keyboard\"; an iPhone keyboard of this kind usually offers only Return.",
|
|
797
|
+
"Several candidates may be listed in one call and it costs nothing, but it still fails when the keyboard carries none of them \u2014 measured with [\"Done\",\"done\",\"Hide keyboard\",\"Dismiss\"].",
|
|
798
|
+
"PRESSING RETURN SUBMITS. An app that commits when the field stops being edited will commit however the keyboard is closed, so check the field afterwards rather than assuming only the keyboard moved.",
|
|
799
|
+
"Confirm with is_keyboard_shown; on Android and on an iPad hide_keyboard closes it on its own."
|
|
800
|
+
],
|
|
801
|
+
"path_params": [
|
|
802
|
+
{
|
|
803
|
+
"name": "session_id",
|
|
804
|
+
"type": "string",
|
|
805
|
+
"required": true,
|
|
806
|
+
"example": "0123456789abcdef0123456789abcdef01234567"
|
|
807
|
+
}
|
|
808
|
+
],
|
|
809
|
+
"body": [
|
|
810
|
+
{
|
|
811
|
+
"name": "script",
|
|
812
|
+
"type": "string",
|
|
813
|
+
"required": true,
|
|
814
|
+
"example": "mobile: hideKeyboard",
|
|
815
|
+
"values": [
|
|
816
|
+
"mobile: hideKeyboard"
|
|
817
|
+
]
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
"name": "args",
|
|
821
|
+
"type": "array",
|
|
822
|
+
"required": true,
|
|
823
|
+
"description": "Exactly one object whose keys field lists the key names to try, in order.",
|
|
824
|
+
"example": [
|
|
825
|
+
{
|
|
826
|
+
"keys": [
|
|
827
|
+
"Done",
|
|
828
|
+
"done",
|
|
829
|
+
"Hide keyboard",
|
|
830
|
+
"Dismiss"
|
|
831
|
+
]
|
|
832
|
+
}
|
|
833
|
+
]
|
|
834
|
+
}
|
|
835
|
+
],
|
|
836
|
+
"returns": [
|
|
837
|
+
"value"
|
|
838
|
+
],
|
|
839
|
+
"responses": {
|
|
840
|
+
"200": {
|
|
841
|
+
"description": "Command result; its shape depends on the command.",
|
|
842
|
+
"schema": {
|
|
843
|
+
"$schema": "AnyValueResponse"
|
|
844
|
+
}
|
|
845
|
+
},
|
|
846
|
+
"404": {
|
|
847
|
+
"$response": "WebDriverError"
|
|
848
|
+
},
|
|
849
|
+
"500": {
|
|
850
|
+
"$response": "WebDriverError"
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
},
|
|
610
854
|
{
|
|
611
855
|
"name": "execute_script",
|
|
612
856
|
"method": "POST",
|
|
@@ -670,7 +914,8 @@
|
|
|
670
914
|
"500": {
|
|
671
915
|
"$response": "WebDriverError"
|
|
672
916
|
}
|
|
673
|
-
}
|
|
917
|
+
},
|
|
918
|
+
"disabled": true
|
|
674
919
|
},
|
|
675
920
|
{
|
|
676
921
|
"name": "get_window_rect",
|
|
@@ -718,7 +963,7 @@
|
|
|
718
963
|
"guidance": [
|
|
719
964
|
"There is no way to resume an ended session; any further step needs create_session again, which reinstalls the app.",
|
|
720
965
|
"The session's video, logs and pass/fail status stay on the App Automate dashboard after it ends.",
|
|
721
|
-
"
|
|
966
|
+
"Mark the result with set_session_status first \u2014 it also works after the session has ended, so this is an ordering preference rather than a last chance."
|
|
722
967
|
],
|
|
723
968
|
"path_params": [
|
|
724
969
|
{
|
|
@@ -729,23 +974,14 @@
|
|
|
729
974
|
}
|
|
730
975
|
],
|
|
731
976
|
"returns": [
|
|
977
|
+
"state",
|
|
732
978
|
"value"
|
|
733
979
|
],
|
|
734
980
|
"responses": {
|
|
735
981
|
"200": {
|
|
736
|
-
"description": "
|
|
982
|
+
"description": "state is \"success\" and value an empty string; the body is the hub's own, not the W3C null.",
|
|
737
983
|
"schema": {
|
|
738
|
-
"
|
|
739
|
-
"properties": {
|
|
740
|
-
"state": {
|
|
741
|
-
"type": "string",
|
|
742
|
-
"description": "\"success\" when the session was ended."
|
|
743
|
-
},
|
|
744
|
-
"value": {
|
|
745
|
-
"type": "string",
|
|
746
|
-
"description": "Empty string."
|
|
747
|
-
}
|
|
748
|
-
}
|
|
984
|
+
"$schema": "SessionDeletedResponse"
|
|
749
985
|
}
|
|
750
986
|
},
|
|
751
987
|
"404": {
|
|
@@ -860,13 +1096,14 @@
|
|
|
860
1096
|
"entity": "app_build",
|
|
861
1097
|
"intent": "Upload an app build to App Automate from a public download link and get back the bs:// URL a session installs \u2014 use this to make a new .apk, .aab or .ipa available to devices.",
|
|
862
1098
|
"guidance": [
|
|
863
|
-
"Only the download-link form can be used here. The same endpoint also accepts a multipart upload carrying the file itself, which this surface cannot send \u2014 so for a build that exists only on the caller's own machine, give them that multipart request to run themselves and take the bs:// url it returns. There is no way to hand the bytes over from here.",
|
|
1099
|
+
"Only the download-link form can be used here. The same endpoint also accepts a multipart upload carrying the file itself, which this surface cannot send \u2014 so for a build that exists only on the caller's own machine, give them that multipart request to run themselves and take the bs:// url it returns. The upload answers with the bs:// url directly: there is no intermediate storage, no presigned url and no second call. There is no way to hand the bytes over from here.",
|
|
864
1100
|
"The url must be https and reachable by BrowserStack without credentials; a link behind a login fails on their side, not the caller's.",
|
|
865
1101
|
"Pass custom_id to give the build a stable name, then later sessions can use that name in place of the bs:// URL and always get the newest build under it.",
|
|
866
1102
|
"Re-uploading under the same custom_id adds a new build rather than replacing the old one; both stay listed.",
|
|
867
1103
|
"Builds expire after 30 days, so a bs:// URL held from an earlier run can stop working.",
|
|
868
1104
|
"Uploading a large build can take minutes \u2014 the call does not return until BrowserStack has fetched the whole file.",
|
|
869
|
-
"upload_app_build_from_file takes a build that is on this machine instead, where the server runs beside the user. It is not offered on a remote server, where a path would name the operator's disk rather than theirs."
|
|
1105
|
+
"upload_app_build_from_file takes a build that is on this machine instead, where the server runs beside the user. It is not offered on a remote server, where a path would name the operator's disk rather than theirs.",
|
|
1106
|
+
"Before uploading at all, check list_app_builds \u2014 a build from an earlier run is still usable for 30 days, and on a remote server reusing one is the only route that needs nothing from the user."
|
|
870
1107
|
],
|
|
871
1108
|
"body": [
|
|
872
1109
|
{
|
|
@@ -913,7 +1150,8 @@
|
|
|
913
1150
|
"guidance": [
|
|
914
1151
|
"An account with no uploads answers with a message object or a 404 rather than an empty array; treat both as 'nothing uploaded yet'.",
|
|
915
1152
|
"This is only the calling user's own uploads \u2014 use list_team_app_builds for everyone on the team.",
|
|
916
|
-
"app_url is what create_session takes; app_id is an internal identifier and is not accepted there."
|
|
1153
|
+
"app_url is what create_session takes; app_id is an internal identifier and is not accepted there.",
|
|
1154
|
+
"On a remote server this is the first thing to try: uploading from a path is not offered there, so an existing build is the only app a session can install without either a public link or the user running an upload by hand."
|
|
917
1155
|
],
|
|
918
1156
|
"query": [
|
|
919
1157
|
{
|
|
@@ -1097,8 +1335,7 @@
|
|
|
1097
1335
|
"Works after the session has ended as well as during it, so the verdict can be set once the result is known.",
|
|
1098
1336
|
"Only 'passed' and 'failed' are accepted; any other value is rejected with a 422.",
|
|
1099
1337
|
"reason is truncated at 255 characters.",
|
|
1100
|
-
"An unset session shows as neither passed nor failed on the dashboard, which reads as an abandoned run."
|
|
1101
|
-
"The same thing can be done from inside a running session with execute_script and the browserstack_executor setSessionStatus action; this way needs no open session."
|
|
1338
|
+
"An unset session shows as neither passed nor failed on the dashboard, which reads as an abandoned run."
|
|
1102
1339
|
],
|
|
1103
1340
|
"path_params": [
|
|
1104
1341
|
{
|
|
@@ -1448,10 +1685,13 @@
|
|
|
1448
1685
|
"entity": "app_session",
|
|
1449
1686
|
"intent": "Close the on-screen keyboard \u2014 use this when it covers a button or field you need to tap after typing.",
|
|
1450
1687
|
"guidance": [
|
|
1451
|
-
"
|
|
1452
|
-
"On iOS this can be refused by a keyboard the app controls; the fallback is tapping an empty area or the keyboard's own Done key.",
|
|
1688
|
+
"On a plain success the response is true when a keyboard was dismissed and false when none was showing; false is not an error. A success reached through the chain below carries the key press's own null value instead.",
|
|
1453
1689
|
"A tap aimed at a control hidden behind the keyboard lands on the keyboard instead and reports success, so close it first rather than afterwards.",
|
|
1454
|
-
"
|
|
1690
|
+
"ON AN iPHONE THE PLAIN CALL IS REFUSED AND A CHAIN RUNS INSTEAD. That keyboard has no dismiss key, so the product answers 400 \"Did not know how to dismiss the keyboard\"; this then presses a key the keyboard actually carries \u2014 Done, Hide keyboard or Dismiss where one exists, and failing that Return. On an iPad the plain call closes it, because that keyboard carries \"Hide keyboard\"; on Android it closes it; neither runs the chain.",
|
|
1691
|
+
"THE RESPONSE LISTS EVERY ATTEMPT WITH ITS STATUS under fallback.attempts, so read which one closed it. When the last attempt's keys include Return, Return is what was pressed \u2014 and in many apps that is a submit: a search runs, a form goes. Read the field and the screen back before assuming only the keyboard moved.",
|
|
1692
|
+
"When no attempt answers 200 the keyboard stays up and the last 400 is the answer: the app offers no way to close it from outside, which a person on the phone would hit too. dismiss_keyboard_key is the same key press on its own, for trying exactly the key you choose \u2014 Return alone, or everything but Return.",
|
|
1693
|
+
"GESTURES DO NOT DISMISS IT. Tapping an empty area above the keyboard returns 200 and changes nothing, and so does a drag downward over it \u2014 including one that starts well above the keyboard's top and moves through it in short steps, which is the form that works on a screen whose scroll view dismisses on drag. This app's does not, and neither gesture moved it.",
|
|
1694
|
+
"WHETHER THE FIELD SURVIVES IS THE APP'S BUSINESS, NOT THE KEYBOARD'S. An app that commits on editing-ended will commit however the keyboard is dismissed \u2014 closing an iPad keyboard this way still moved the text out of the field. So do not expect any dismissal to be side-effect free; check the app's own behaviour. On Android it closes the keyboard, answers true, and leaves the field as it was.",
|
|
1455
1695
|
"is_keyboard_shown reports the state this changes, and is the reliable way to tell whether the keyboard actually went away."
|
|
1456
1696
|
],
|
|
1457
1697
|
"path_params": [
|
|
@@ -1462,6 +1702,50 @@
|
|
|
1462
1702
|
"example": "0123456789abcdef0123456789abcdef01234567"
|
|
1463
1703
|
}
|
|
1464
1704
|
],
|
|
1705
|
+
"fallbacks": [
|
|
1706
|
+
{
|
|
1707
|
+
"when": {
|
|
1708
|
+
"status": 400
|
|
1709
|
+
},
|
|
1710
|
+
"capability": "dismiss_keyboard_key",
|
|
1711
|
+
"body": {
|
|
1712
|
+
"script": "mobile: hideKeyboard",
|
|
1713
|
+
"args": [
|
|
1714
|
+
{
|
|
1715
|
+
"keys": [
|
|
1716
|
+
"Done",
|
|
1717
|
+
"done",
|
|
1718
|
+
"Hide keyboard",
|
|
1719
|
+
"Dismiss"
|
|
1720
|
+
]
|
|
1721
|
+
}
|
|
1722
|
+
]
|
|
1723
|
+
}
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
"when": {
|
|
1727
|
+
"status": 400
|
|
1728
|
+
},
|
|
1729
|
+
"capability": "dismiss_keyboard_key",
|
|
1730
|
+
"body": {
|
|
1731
|
+
"script": "mobile: hideKeyboard",
|
|
1732
|
+
"args": [
|
|
1733
|
+
{
|
|
1734
|
+
"keys": [
|
|
1735
|
+
"Return",
|
|
1736
|
+
"return",
|
|
1737
|
+
"Go",
|
|
1738
|
+
"go",
|
|
1739
|
+
"Next",
|
|
1740
|
+
"next",
|
|
1741
|
+
"Search",
|
|
1742
|
+
"search"
|
|
1743
|
+
]
|
|
1744
|
+
}
|
|
1745
|
+
]
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
],
|
|
1465
1749
|
"returns": [
|
|
1466
1750
|
"value"
|
|
1467
1751
|
],
|
|
@@ -1690,11 +1974,11 @@
|
|
|
1690
1974
|
"entity": "app_session",
|
|
1691
1975
|
"intent": "Press a hardware or system key on an Android device by its key code \u2014 use this for home, enter, delete, volume or any key with no on-screen control.",
|
|
1692
1976
|
"guidance": [
|
|
1693
|
-
"Android only. iOS has no equivalent: use press_back to go back, and
|
|
1977
|
+
"Android only. iOS has no equivalent: use press_back to go back, and press_button for home and the volume keys.",
|
|
1694
1978
|
"Common codes: 3 home, 4 back, 66 enter, 67 delete, 61 tab, 82 menu, 84 search, 187 app switch, 24 volume up, 25 volume down, 26 power.",
|
|
1695
1979
|
"press_back is the clearer way to send 4.",
|
|
1696
1980
|
"The response is null on success and says nothing about what the key did.",
|
|
1697
|
-
"Called on iOS this is a 405 \"unknown method\" rather than a silent no-op; use press_back or
|
|
1981
|
+
"Called on iOS this is a 405 \"unknown method\" rather than a silent no-op; use press_back or press_button there."
|
|
1698
1982
|
],
|
|
1699
1983
|
"path_params": [
|
|
1700
1984
|
{
|
|
@@ -1752,7 +2036,7 @@
|
|
|
1752
2036
|
"Returns the Android package name; it answers with the launcher's package when the app has been sent to the background.",
|
|
1753
2037
|
"This is the quickest way to learn the app under test's own package id, which launch_app and terminate_app both need.",
|
|
1754
2038
|
"Android only; get_active_app_info is the iOS equivalent and returns an object rather than a string.",
|
|
1755
|
-
"Called on iOS this is a 405 \"unknown method\" rather than a silent no-op; use
|
|
2039
|
+
"Called on iOS this is a 405 \"unknown method\" rather than a silent no-op; use get_active_app_info there."
|
|
1756
2040
|
],
|
|
1757
2041
|
"path_params": [
|
|
1758
2042
|
{
|
|
@@ -1807,7 +2091,7 @@
|
|
|
1807
2091
|
"entity": "app_session",
|
|
1808
2092
|
"intent": "Bring an app to the foreground, starting it if it is not running \u2014 use this to open the app under test, return to it, or switch to another installed app.",
|
|
1809
2093
|
"guidance": [
|
|
1810
|
-
"
|
|
2094
|
+
"The app is named INSIDE `args`, which is the one object there: args [{\"appId\": \"com.example.app\"}] on Android, args [{\"bundleId\": \"com.example.app\"}] on iOS. Sending appId or bundleId as a top-level body field is refused here before anything is sent \u2014 \"unknown body: bundleId. accepted: args, script\" \u2014 and sending the wrong one for the platform is refused by the product with a 400 \"invalid argument\" naming the parameter it wanted, with no action taken.",
|
|
1811
2095
|
"Launching an app that is already in the foreground does nothing and still reports success.",
|
|
1812
2096
|
"To restart an app from a clean state, call terminate_app first and then this.",
|
|
1813
2097
|
"Succeeding means the app was activated, not that it finished drawing, so give it a moment and confirm before acting. An app that cannot be brought forward at all answers 400 rather than reporting success, and get_app_state reads 4 once it really is in front \u2014 sampled too early it can still report the home screen mid-switch.",
|
|
@@ -1880,7 +2164,7 @@
|
|
|
1880
2164
|
"entity": "app_session",
|
|
1881
2165
|
"intent": "Close an app and stop it running \u2014 use this to end a session inside the app, or as the first half of a restart.",
|
|
1882
2166
|
"guidance": [
|
|
1883
|
-
"
|
|
2167
|
+
"The app is named INSIDE `args`, which is the one object there: args [{\"appId\": \"com.example.app\"}] on Android, args [{\"bundleId\": \"com.example.app\"}] on iOS. Sending appId or bundleId as a top-level body field is refused here before anything is sent \u2014 \"unknown body: bundleId. accepted: args, script\" \u2014 and sending the wrong one for the platform is refused by the product with a 400 \"invalid argument\" naming the parameter it wanted, with no action taken.",
|
|
1884
2168
|
"Returns true when the app was running and false when it was not; false is not an error.",
|
|
1885
2169
|
"The app stays installed and keeps its data; only the running process is stopped.",
|
|
1886
2170
|
"This does not clear state between runs \u2014 relaunching shows the app where it was unless the app itself resets."
|
|
@@ -1952,7 +2236,7 @@
|
|
|
1952
2236
|
"entity": "app_session",
|
|
1953
2237
|
"intent": "Check whether an app is installed, running or in the foreground \u2014 use this to confirm a launch or a close actually took effect.",
|
|
1954
2238
|
"guidance": [
|
|
1955
|
-
"
|
|
2239
|
+
"The app is named INSIDE `args`, which is the one object there: args [{\"appId\": \"com.example.app\"}] on Android, args [{\"bundleId\": \"com.example.app\"}] on iOS. Sending appId or bundleId as a top-level body field is refused here before anything is sent \u2014 \"unknown body: bundleId. accepted: args, script\" \u2014 and sending the wrong one for the platform is refused by the product with a 400 \"invalid argument\" naming the parameter it wanted, with no action taken.",
|
|
1956
2240
|
"The value is a number: 0 not installed, 1 installed but not running, 2 running in the background and suspended, 3 running in the background, 4 running in the foreground.",
|
|
1957
2241
|
"4 is the only state in which the app is on screen and can be driven.",
|
|
1958
2242
|
"The same scale applies on both platforms. Sending an app to the background leaves it at 3 or 2 \u2014 the system suspends it a moment after it stops being visible \u2014 so treat anything below 4 as not on screen rather than waiting for one number. Launching it again returns it to 4."
|
|
@@ -2365,7 +2649,7 @@
|
|
|
2365
2649
|
"guidance": [
|
|
2366
2650
|
"iOS only; press_keycode is the Android equivalent and the two are not interchangeable.",
|
|
2367
2651
|
"The button names carry no separator: home, volumeup, volumedown.",
|
|
2368
|
-
"Pressing home sends the app to the background \u2014 bring it back with launch_app,
|
|
2652
|
+
"Pressing home sends the app to the background \u2014 bring it back with launch_app, whose args object takes bundleId rather than appId on iOS, and confirm from the page source that the app is in front again.",
|
|
2369
2653
|
"The response is null on success and says nothing about what the button did."
|
|
2370
2654
|
],
|
|
2371
2655
|
"path_params": [
|
|
@@ -2547,7 +2831,8 @@
|
|
|
2547
2831
|
"Only `getprop <property>` can be expressed: the accepted form is fixed, and a command that lists apps or system state cannot be sent through this capability. A public phone is shared between customers, and commands such as `pm list packages` or `dumpsys` report what other people's sessions left behind.",
|
|
2548
2832
|
"BrowserStack applies its own allow-list on top of this one and accepts only some properties on a shared phone; ro.build.version.release and ro.product.manufacturer are accepted, while a bare getprop is not. A property it refuses comes back as an execution failure rather than an empty value.",
|
|
2549
2833
|
"The device name and OS version are already in the session record, so get_session answers those without this.",
|
|
2550
|
-
"The whole line must be sent exactly as shown, with no extra spaces: it is matched against a fixed form."
|
|
2834
|
+
"The whole line must be sent exactly as shown, with no extra spaces: it is matched against a fixed form.",
|
|
2835
|
+
"The allowed property list is narrow and locale is not on it: getprop persist.sys.locale and getprop ro.product.locale both come back HTTP 500, \"BROWSERSTACK_UNSUPPORTED_ADB_SHELL_COMMAND ... is not supported on BrowserStack currently\". ro.build.version.release and ro.product.manufacturer do work, so treat the refusal as a per-property allow-list rather than a broken capability."
|
|
2551
2836
|
],
|
|
2552
2837
|
"path_params": [
|
|
2553
2838
|
{
|
|
@@ -2599,14 +2884,25 @@
|
|
|
2599
2884
|
"encoding": "multipart",
|
|
2600
2885
|
"local_only": true,
|
|
2601
2886
|
"intent": "Upload an app build that is on this machine to App Automate and get back the bs:// URL a session installs \u2014 use this when the .apk, .aab or .ipa is a local file rather than a download link.",
|
|
2602
|
-
"guidance":
|
|
2603
|
-
"
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
"
|
|
2609
|
-
|
|
2887
|
+
"guidance": {
|
|
2888
|
+
"shared": [
|
|
2889
|
+
"Only .apk, .aab and .ipa are accepted.",
|
|
2890
|
+
"A large build takes minutes, because the upload does not return until the whole file has been sent.",
|
|
2891
|
+
"upload_app_build takes a public https link instead, and list_app_builds reuses a build already on the account \u2014 builds last 30 days."
|
|
2892
|
+
],
|
|
2893
|
+
"local": [
|
|
2894
|
+
"The file must sit inside the directory MCP_UPLOAD_BASE_DIR names. When that is not set the upload is refused and nothing is sent: tell the user to set it to the folder holding their builds and restart the server.",
|
|
2895
|
+
"When it is set but the build is somewhere else, the refusal names the directory the file has to be inside. Tell the user to move it there rather than trying another path.",
|
|
2896
|
+
"No part of the path may be a hidden folder, so a build under a dot-directory has to be moved out of it.",
|
|
2897
|
+
"The path is checked before anything is uploaded and before the user is asked to approve, so a refusal costs nothing and means the file was never read."
|
|
2898
|
+
],
|
|
2899
|
+
"remote": [
|
|
2900
|
+
"Cheapest route first: list_app_builds or list_team_app_builds, if the build was uploaded before.",
|
|
2901
|
+
"Then upload_app_build, if the build sits at a public https link \u2014 the only route that needs nothing from the user.",
|
|
2902
|
+
"Otherwise the user uploads it themselves. The asset-upload helper PRINTS a curl command and nothing more \u2014 it reads no file and sends no request \u2014 so the user runs that command, and it answers with the bs:// url directly from /app-automate/upload. There is no intermediate storage and no second step.",
|
|
2903
|
+
"Take the bs:// url back from them and pass it as the app for create_session."
|
|
2904
|
+
]
|
|
2905
|
+
},
|
|
2610
2906
|
"body": [
|
|
2611
2907
|
{
|
|
2612
2908
|
"name": "file",
|
|
@@ -2638,8 +2934,7 @@
|
|
|
2638
2934
|
"401": {
|
|
2639
2935
|
"$response": "Unauthorized"
|
|
2640
2936
|
}
|
|
2641
|
-
}
|
|
2642
|
-
"unavailable_note": "This server does not run on your own machine, so it cannot read a file you name \u2014 a path sent here would point at the machine hosting the server. Upload the build yourself with a multipart request carrying the file to the same endpoint, which the asset-upload helper will write out for you, and then use the bs:// url it returns as the app for create_session. upload_app_build does the same thing from a public download link, if the build is reachable at one."
|
|
2937
|
+
}
|
|
2643
2938
|
},
|
|
2644
2939
|
{
|
|
2645
2940
|
"name": "find_elements",
|
|
@@ -3145,6 +3440,24 @@
|
|
|
3145
3440
|
}
|
|
3146
3441
|
}
|
|
3147
3442
|
}
|
|
3443
|
+
},
|
|
3444
|
+
"SessionDeletedResponse": {
|
|
3445
|
+
"type": "object",
|
|
3446
|
+
"description": "What the hub returns when a session is closed. Not the W3C null: state says whether it closed and value is an empty string.",
|
|
3447
|
+
"properties": {
|
|
3448
|
+
"state": {
|
|
3449
|
+
"type": "string",
|
|
3450
|
+
"description": "\"success\" when the session was closed."
|
|
3451
|
+
},
|
|
3452
|
+
"status": {
|
|
3453
|
+
"type": "integer",
|
|
3454
|
+
"description": "The hub's own legacy status code; 0 on success."
|
|
3455
|
+
},
|
|
3456
|
+
"value": {
|
|
3457
|
+
"type": "string",
|
|
3458
|
+
"description": "Always empty on success."
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3148
3461
|
}
|
|
3149
3462
|
},
|
|
3150
3463
|
"responses": {
|
|
@@ -3220,7 +3533,11 @@
|
|
|
3220
3533
|
"get_simulated_location",
|
|
3221
3534
|
"get_active_app_info",
|
|
3222
3535
|
"read_device_property",
|
|
3223
|
-
"find_elements"
|
|
3536
|
+
"find_elements",
|
|
3537
|
+
"clear_element",
|
|
3538
|
+
"scroll_gesture",
|
|
3539
|
+
"dismiss_keyboard_key",
|
|
3540
|
+
"scroll_view"
|
|
3224
3541
|
]
|
|
3225
3542
|
},
|
|
3226
3543
|
"device": {
|