mirror of
https://github.com/httprunner/httprunner.git
synced 2026-07-07 23:41:22 +08:00
Merge branch 'session_refactor' into 'master'
feat: 支持获取剪贴板 See merge request iesqa/httprunner!135
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1166,6 +1167,59 @@ func (ad *ADBDriver) ClearFiles(paths ...string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ad *ADBDriver) GetPasteboard() (content string, err error) {
|
||||||
|
/**
|
||||||
|
adb shell am broadcast -n io.appium.settings/.receivers.ClipboardReceiver -a io.appium.settings.clipboard.get
|
||||||
|
Broadcasting: Intent { act=io.appium.settings.clipboard.get flg=0x400000 cmp=io.appium.settings/.receivers.ClipboardReceiver }
|
||||||
|
Broadcast completed: result=-1, data="SEhHRw=="
|
||||||
|
**/
|
||||||
|
|
||||||
|
// Check and switch input method if needed, similar to SendUnicodeKeys
|
||||||
|
currentIme, err := ad.GetIme()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If current IME is not the required one, switch temporarily and restore later
|
||||||
|
if currentIme != option.UnicodeImePackageName {
|
||||||
|
defer func() {
|
||||||
|
_ = ad.SetIme(currentIme)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = ad.SetIme(option.UnicodeImePackageName)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("set Unicode Ime failed for clipboard operation")
|
||||||
|
// Continue anyway, might still work with current IME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ad.Device.RunShellCommand("am", "broadcast", "-n", option.AppiumSettingsPackageName+"/.receivers.ClipboardReceiver", "-a", option.AppiumSettingsPackageName+".clipboard.get")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the response to extract the base64 encoded data
|
||||||
|
dataPrefix := "data=\""
|
||||||
|
dataIndex := strings.Index(res, dataPrefix)
|
||||||
|
if dataIndex == -1 {
|
||||||
|
return "", fmt.Errorf("clipboard data not found in response: %s", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
dataStart := dataIndex + len(dataPrefix)
|
||||||
|
dataEnd := strings.Index(res[dataStart:], "\"")
|
||||||
|
if dataEnd == -1 {
|
||||||
|
return "", fmt.Errorf("malformed clipboard data in response: %s", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
base64Data := res[dataStart : dataStart+dataEnd]
|
||||||
|
decodedData, err := base64.StdEncoding.DecodeString(base64Data)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode clipboard content: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(decodedData), nil
|
||||||
|
}
|
||||||
|
|
||||||
type ExportPoint struct {
|
type ExportPoint struct {
|
||||||
Start int `json:"start" yaml:"start"`
|
Start int `json:"start" yaml:"start"`
|
||||||
End int `json:"end" yaml:"end"`
|
End int `json:"end" yaml:"end"`
|
||||||
|
|||||||
@@ -574,32 +574,6 @@ func (ud *UIA2Driver) SetPasteboard(contentType types.PasteboardType, content st
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ud *UIA2Driver) GetPasteboard(contentType types.PasteboardType) (raw *bytes.Buffer, err error) {
|
|
||||||
if len(contentType) == 0 {
|
|
||||||
contentType = types.PasteboardTypePlaintext
|
|
||||||
}
|
|
||||||
// register(postHandler, new GetClipboard("/wd/hub/session/:sessionId/appium/device/get_clipboard"))
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"contentType": contentType[0],
|
|
||||||
}
|
|
||||||
var rawResp DriverRawResponse
|
|
||||||
urlStr := fmt.Sprintf("/session/%s/appium/device/get_clipboard", ud.Session.ID)
|
|
||||||
if rawResp, err = ud.Session.POST(data, urlStr); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reply := new(struct{ Value string })
|
|
||||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if data, err := base64.StdEncoding.DecodeString(reply.Value); err != nil {
|
|
||||||
raw.Write([]byte(reply.Value))
|
|
||||||
} else {
|
|
||||||
raw.Write(data)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendKeys Android input does not support setting frequency.
|
// SendKeys Android input does not support setting frequency.
|
||||||
func (ud *UIA2Driver) Input(text string, opts ...option.ActionOption) (err error) {
|
func (ud *UIA2Driver) Input(text string, opts ...option.ActionOption) (err error) {
|
||||||
log.Info().Str("text", text).Msg("UIA2Driver.Input")
|
log.Info().Str("text", text).Msg("UIA2Driver.Input")
|
||||||
|
|||||||
@@ -321,3 +321,10 @@ func TestDriver_UIA2_Input(t *testing.T) {
|
|||||||
err = driver.Input("123\n")
|
err = driver.Input("123\n")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDriver_ADB_GetPasteboard(t *testing.T) {
|
||||||
|
driver := setupADBDriverExt(t)
|
||||||
|
pasteboard, err := driver.IDriver.(*ADBDriver).GetPasteboard()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
t.Log(pasteboard)
|
||||||
|
}
|
||||||
|
|||||||
@@ -365,7 +365,6 @@ func (wd *BrowserDriver) Input(text string, option ...option.ActionOption) (err
|
|||||||
// Source Return application elements tree
|
// Source Return application elements tree
|
||||||
func (wd *BrowserDriver) Source(srcOpt ...option.SourceOption) (string, error) {
|
func (wd *BrowserDriver) Source(srcOpt ...option.SourceOption) (string, error) {
|
||||||
result, err := wd.CustomeGet(wd.concatURL(wd.Session.ID, "stub/source"))
|
result, err := wd.CustomeGet(wd.concatURL(wd.Session.ID, "stub/source"))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -751,3 +750,7 @@ func (wd *BrowserDriver) CustomeGet(urlStr string) (response *WebAgentResponse,
|
|||||||
|
|
||||||
return &webResp, err
|
return &webResp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wd *BrowserDriver) GetPasteboard() (content string, err error) {
|
||||||
|
return "", errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|||||||
@@ -86,4 +86,7 @@ type IDriver interface {
|
|||||||
// triggers the log capture and returns the log entries
|
// triggers the log capture and returns the log entries
|
||||||
StartCaptureLog(identifier ...string) error
|
StartCaptureLog(identifier ...string) error
|
||||||
StopCaptureLog() (result interface{}, err error)
|
StopCaptureLog() (result interface{}, err error)
|
||||||
|
|
||||||
|
// clipboard operations
|
||||||
|
GetPasteboard() (string, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -348,3 +348,7 @@ func (hd *HDCDriver) SecondaryClick(x, y float64) (err error) {
|
|||||||
func (hd *HDCDriver) SecondaryClickBySelector(selector string, options ...option.ActionOption) (err error) {
|
func (hd *HDCDriver) SecondaryClickBySelector(selector string, options ...option.ActionOption) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hd *HDCDriver) GetPasteboard() (content string, err error) {
|
||||||
|
return "", errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|||||||
@@ -630,17 +630,22 @@ func (wd *WDADriver) SetPasteboard(contentType types.PasteboardType, content str
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wd *WDADriver) GetPasteboard(contentType types.PasteboardType) (raw *bytes.Buffer, err error) {
|
func (wd *WDADriver) GetPasteboard() (content string, err error) {
|
||||||
// [[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)]
|
// [[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)]
|
||||||
data := map[string]interface{}{"contentType": contentType}
|
err = wd.AppLaunch("com.gtf.wda.runner.xctrunner")
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "GetPasteboard failed. WDA app not launched")
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{}
|
||||||
var rawResp DriverRawResponse
|
var rawResp DriverRawResponse
|
||||||
if rawResp, err = wd.Session.POST(data, "/wda/getPasteboard"); err != nil {
|
if rawResp, err = wd.Session.POST(data, "/wda/getPasteboard"); err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
var raw *bytes.Buffer
|
||||||
if raw, err = rawResp.ValueDecodeAsBase64(); err != nil {
|
if raw, err = rawResp.ValueDecodeAsBase64(); err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
return
|
return string(raw.Bytes()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wd *WDADriver) SetIme(ime string) error {
|
func (wd *WDADriver) SetIme(ime string) error {
|
||||||
|
|||||||
@@ -356,3 +356,10 @@ func TestDriver_WDA_PushImage(t *testing.T) {
|
|||||||
err = driver.ClearImages()
|
err = driver.ClearImages()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDriver_WDA_GetPasteboard(t *testing.T) {
|
||||||
|
driver := setupWDADriverExt(t)
|
||||||
|
pasteboard, err := driver.IDriver.(*WDADriver).GetPasteboard()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
t.Log(pasteboard)
|
||||||
|
}
|
||||||
|
|||||||
56
uixt/mcp_tools_pasteboard.go
Normal file
56
uixt/mcp_tools_pasteboard.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package uixt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToolGetPasteboard implements the get_pasteboard tool call.
|
||||||
|
type ToolGetPasteboard struct {
|
||||||
|
// Return data fields - these define the structure of data returned by this tool
|
||||||
|
Content string `json:"content" desc:"Clipboard content that was retrieved"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToolGetPasteboard) Name() option.ActionName {
|
||||||
|
return option.ACTION_GetPasteboard
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToolGetPasteboard) Description() string {
|
||||||
|
return "Get the clipboard content from the device"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToolGetPasteboard) Options() []mcp.ToolOption {
|
||||||
|
unifiedReq := &option.ActionOptions{}
|
||||||
|
return unifiedReq.GetMCPOptions(option.ACTION_GetPasteboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToolGetPasteboard) Implement() server.ToolHandlerFunc {
|
||||||
|
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
arguments := request.GetArguments()
|
||||||
|
driverExt, err := setupXTDriver(ctx, arguments)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("setup driver failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directly call the GetPasteboard method on the driver
|
||||||
|
content, err := driverExt.IDriver.GetPasteboard()
|
||||||
|
if err != nil {
|
||||||
|
return NewMCPErrorResponse(fmt.Sprintf("Get pasteboard failed: %s", err.Error())), err
|
||||||
|
}
|
||||||
|
|
||||||
|
message := "Successfully retrieved clipboard content"
|
||||||
|
returnData := ToolGetPasteboard{Content: content}
|
||||||
|
|
||||||
|
return NewMCPSuccessResponse(message, &returnData), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ToolGetPasteboard) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
|
||||||
|
arguments := map[string]any{}
|
||||||
|
return BuildMCPCallToolRequest(t.Name(), arguments, action), nil
|
||||||
|
}
|
||||||
@@ -55,7 +55,8 @@ const (
|
|||||||
ACTION_SetIme ActionName = "set_ime"
|
ACTION_SetIme ActionName = "set_ime"
|
||||||
ACTION_GetSource ActionName = "get_source"
|
ACTION_GetSource ActionName = "get_source"
|
||||||
ACTION_GetForegroundApp ActionName = "get_foreground_app"
|
ACTION_GetForegroundApp ActionName = "get_foreground_app"
|
||||||
ACTION_AppInfo ActionName = "app_info" // get app info action
|
ACTION_GetPasteboard ActionName = "get_pasteboard" // get clipboard content
|
||||||
|
ACTION_AppInfo ActionName = "app_info" // get app info action
|
||||||
|
|
||||||
// UI handling
|
// UI handling
|
||||||
ACTION_Home ActionName = "home"
|
ACTION_Home ActionName = "home"
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ const (
|
|||||||
defaultUIA2ServerPackageName = "io.appium.uiautomator2.server"
|
defaultUIA2ServerPackageName = "io.appium.uiautomator2.server"
|
||||||
defaultUIA2ServerTestPackageName = "io.appium.uiautomator2.server.test"
|
defaultUIA2ServerTestPackageName = "io.appium.uiautomator2.server.test"
|
||||||
|
|
||||||
AdbKeyBoardPackageName = "com.android.adbkeyboard/.AdbIME"
|
AdbKeyBoardPackageName = "com.android.adbkeyboard/.AdbIME"
|
||||||
UnicodeImePackageName = "io.appium.settings/.UnicodeIME"
|
UnicodeImePackageName = AppiumSettingsPackageName + "/.UnicodeIME"
|
||||||
|
AppiumSettingsPackageName = "io.appium.settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAndroidDeviceOptions(opts ...AndroidDeviceOption) *AndroidDeviceOptions {
|
func NewAndroidDeviceOptions(opts ...AndroidDeviceOption) *AndroidDeviceOptions {
|
||||||
|
|||||||
Reference in New Issue
Block a user