diff --git a/internal/version/VERSION b/internal/version/VERSION
index d3845ad3..35e28788 100644
--- a/internal/version/VERSION
+++ b/internal/version/VERSION
@@ -1 +1 @@
-v5.0.0
+v5.0.0-2506271152
diff --git a/report.go b/report.go
index 54f8bee0..3f304175 100644
--- a/report.go
+++ b/report.go
@@ -12,11 +12,12 @@ import (
"strings"
"time"
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog/log"
+
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option"
- "github.com/pkg/errors"
- "github.com/rs/zerolog/log"
)
// GenerateHTMLReportFromFiles is a convenience function to generate HTML report
@@ -553,13 +554,14 @@ func (g *HTMLReportGenerator) GenerateReport(outputFile string) error {
"safeHTML": func(s string) template.HTML {
return template.HTML(s)
},
- "toJSON": func(v any) string {
+ "toJSONFormatted": func(v any) string {
var buf strings.Builder
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
+ encoder.SetIndent("", " ")
_ = encoder.Encode(v)
- result := buf.String()
- return strings.TrimSpace(result)
+ result := strings.TrimSpace(buf.String())
+ return result
},
"add": func(a, b int) int { return a + b },
"base": filepath.Base,
@@ -584,6 +586,20 @@ func (g *HTMLReportGenerator) GenerateReport(outputFile string) error {
// If not JSON or no thought field, return original content
return content
},
+ "formatBodyContent": func(content string) string {
+ // Try to parse as JSON to format
+ var data interface{}
+ if err := json.Unmarshal([]byte(content), &data); err == nil {
+ var buf strings.Builder
+ encoder := json.NewEncoder(&buf)
+ encoder.SetEscapeHTML(false)
+ encoder.SetIndent("", " ")
+ _ = encoder.Encode(data)
+ return strings.TrimSpace(buf.String())
+ }
+ // If not JSON, return original content
+ return content
+ },
}
// Parse template
@@ -1372,9 +1388,12 @@ const htmlTemplate = `
margin: 2px 0;
font-family: monospace;
font-size: 0.7em;
- max-height: 60px;
+ max-height: 80px;
overflow-y: auto;
word-break: break-all;
+ white-space: nowrap;
+ overflow-x: auto;
+ line-height: 1.3;
}
.model-output-compact {
@@ -1504,12 +1523,6 @@ const htmlTemplate = `
font-weight: 600;
}
- .screenshots-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 10px;
- }
-
.screenshots-horizontal {
display: flex;
gap: 15px;
@@ -1903,26 +1916,6 @@ const htmlTemplate = `
overflow: hidden;
}
- .controls {
- text-align: center;
- margin-bottom: 20px;
- }
-
- .controls button {
- background: #007bff;
- color: white;
- border: none;
- padding: 8px 16px;
- border-radius: 4px;
- margin: 0 5px;
- cursor: pointer;
- transition: background-color 0.3s;
- }
-
- .controls button:hover {
- background: #0056b3;
- }
-
/* Modal styles */
.modal {
display: none;
@@ -2008,6 +2001,68 @@ const htmlTemplate = `
border-radius: 0 0 12px 12px;
}
+ /* JSON Syntax Highlighting */
+ .json-key {
+ color: #0066cc;
+ font-weight: bold;
+ }
+
+ .json-string {
+ color: #22863a;
+ }
+
+ .json-number {
+ color: #e36209;
+ }
+
+ .json-boolean {
+ color: #d73a49;
+ font-weight: bold;
+ }
+
+ .json-null {
+ color: #6f42c1;
+ font-weight: bold;
+ }
+
+ .json-punctuation {
+ color: #24292e;
+ }
+
+ .json-brace {
+ color: #586069;
+ font-weight: bold;
+ }
+
+ .json-bracket {
+ color: #586069;
+ font-weight: bold;
+ }
+
+ /* Inline JSON highlighting for smaller displays */
+ .json-inline .json-key {
+ color: #0066cc;
+ font-weight: 600;
+ }
+
+ .json-inline .json-string {
+ color: #22863a;
+ }
+
+ .json-inline .json-number {
+ color: #e36209;
+ }
+
+ .json-inline .json-boolean {
+ color: #d73a49;
+ font-weight: 600;
+ }
+
+ .json-inline .json-null {
+ color: #6f42c1;
+ font-weight: 600;
+ }
+
.json-toolbar {
background: #e9ecef;
padding: 10px 20px;
@@ -2290,21 +2345,6 @@ const htmlTemplate = `
font-size: 0.95em;
}
- .action-output {
- background: #f8f9fa;
- border: 2px solid #6f42c1;
- border-radius: 6px;
- padding: 10px;
- font-size: 0.85em;
- max-height: 120px;
- overflow-y: auto;
- white-space: pre-wrap;
- word-wrap: break-word;
- color: #495057;
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- line-height: 1.4;
- }
-
.action-session-data {
margin-top: 15px;
padding: 15px;
@@ -2538,7 +2578,7 @@ const htmlTemplate = `
🔧 Tool Calls: {{$planning.ToolCallsCount}}
{{end}}
{{if $planning.ActionNames}}
- 🎯 Actions: {{safeHTML (toJSON $planning.ActionNames)}}
+ 🎯 Actions: {{toJSONFormatted $planning.ActionNames}}
{{end}}
@@ -2560,7 +2600,9 @@ const htmlTemplate = `
{{if $subAction.Error}}❌{{else}}✅{{end}}
{{if $subAction.Arguments}}
- {{safeHTML (toJSON $subAction.Arguments)}}
+
+ {{toJSONFormatted $subAction.Arguments}}
+
{{end}}
{{if $subAction.Requests}}
@@ -2668,7 +2710,7 @@ const htmlTemplate = `
{{/* Display structured data for query results */}}
{{if $action.AIResult.QueryResult.Data}}
📥 Structured Data:
-
{{safeHTML (toJSON $action.AIResult.QueryResult.Data)}}
+
{{toJSONFormatted $action.AIResult.QueryResult.Data}}
{{end}}
{{else if eq $action.AIResult.Type "action"}}
{{if $action.AIResult.PlanningResult.ModelName}}
@@ -2850,7 +2892,9 @@ const htmlTemplate = `
{{end}}
{{if $logEntry.Fields}}
- {{safeHTML (toJSON $logEntry.Fields)}}
+
+ {{toJSONFormatted $logEntry.Fields}}
+
{{end}}
{{end}}
@@ -2958,6 +3002,121 @@ const htmlTemplate = `
const logContent = decodeBase64UTF8(logContentBase64);
const caseContent = decodeBase64UTF8(caseContentBase64);
+ // Enhanced JSON highlighting with better parsing
+ function highlightJSONAdvanced(jsonString) {
+ if (!jsonString || typeof jsonString !== 'string') {
+ return jsonString;
+ }
+
+ let result = '';
+ let i = 0;
+ let inString = false;
+ let inKey = false;
+ let escaped = false;
+
+ while (i < jsonString.length) {
+ const char = jsonString[i];
+ const nextChar = jsonString[i + 1];
+
+ if (escaped) {
+ result += char;
+ escaped = false;
+ i++;
+ continue;
+ }
+
+ if (char === '\\' && inString) {
+ escaped = true;
+ result += char;
+ i++;
+ continue;
+ }
+
+ if (char === '"') {
+ if (!inString) {
+ // Starting a string
+ inString = true;
+ // Check if this is a key (followed by colon)
+ let j = i + 1;
+ let tempStr = '';
+ let tempEscaped = false;
+ while (j < jsonString.length) {
+ const c = jsonString[j];
+ if (tempEscaped) {
+ tempEscaped = false;
+ j++;
+ continue;
+ }
+ if (c === '\\') {
+ tempEscaped = true;
+ j++;
+ continue;
+ }
+ if (c === '"') {
+ // End of string, check what follows
+ j++;
+ while (j < jsonString.length && /\s/.test(jsonString[j])) j++;
+ if (j < jsonString.length && jsonString[j] === ':') {
+ inKey = true;
+ }
+ break;
+ }
+ j++;
+ }
+
+ if (inKey) {
+ result += '"';
+ } else {
+ result += '"';
+ }
+ } else {
+ // Ending a string
+ inString = false;
+ result += '"';
+ inKey = false;
+ }
+ } else if (!inString) {
+ // Handle non-string content
+ if (char === ':') {
+ result += ':';
+ } else if (char === ',') {
+ result += ',';
+ } else if (char === '{' || char === '}') {
+ result += '' + char + '';
+ } else if (char === '[' || char === ']') {
+ result += '' + char + '';
+ } else if (/\d/.test(char) || (char === '-' && /\d/.test(nextChar))) {
+ // Handle numbers
+ let numStr = '';
+ while (i < jsonString.length && /[\d\.\-\+e]/i.test(jsonString[i])) {
+ numStr += jsonString[i];
+ i++;
+ }
+ result += '' + numStr + '';
+ i--; // Adjust for the loop increment
+ } else if (char === 't' && jsonString.substr(i, 4) === 'true') {
+ result += 'true';
+ i += 3; // Skip the rest of 'true'
+ } else if (char === 'f' && jsonString.substr(i, 5) === 'false') {
+ result += 'false';
+ i += 4; // Skip the rest of 'false'
+ } else if (char === 'n' && jsonString.substr(i, 4) === 'null') {
+ result += 'null';
+ i += 3; // Skip the rest of 'null'
+ } else {
+ result += char;
+ }
+ } else {
+ // Inside string, just add character
+ result += char;
+ }
+
+ i++;
+ }
+
+ return result;
+ }
+
// Download functions
function downloadSummary() {
if (!summaryContent) {
@@ -2997,9 +3156,11 @@ const htmlTemplate = `
try {
// Parse and format JSON for beautiful display
const jsonObj = JSON.parse(caseContent);
- const formattedJson = JSON.stringify(jsonObj, null, 2);
+ const formattedJson = JSON.stringify(jsonObj, null, 4);
- document.getElementById('jsonContent').textContent = formattedJson;
+ // Apply syntax highlighting
+ const highlightedJson = highlightJSONAdvanced(formattedJson);
+ document.getElementById('jsonContent').innerHTML = highlightedJson;
document.getElementById('jsonModal').style.display = 'block';
} catch (e) {
console.error('Failed to parse JSON:', e);
@@ -3014,22 +3175,39 @@ const htmlTemplate = `
}
function copyJsonContent() {
- const jsonContent = document.getElementById('jsonContent').textContent;
- if (!jsonContent) {
+ // Copy the original formatted JSON content instead of highlighted HTML
+ if (!caseContent) {
alert('No content to copy');
return;
}
- navigator.clipboard.writeText(jsonContent).then(function() {
- const copyStatus = document.getElementById('copyStatus');
- copyStatus.classList.add('show');
- setTimeout(function() {
- copyStatus.classList.remove('show');
- }, 2000);
- }).catch(function(err) {
- console.error('Failed to copy to clipboard:', err);
- alert('Failed to copy to clipboard. Please select and copy manually.');
- });
+ try {
+ const jsonObj = JSON.parse(caseContent);
+ const formattedJson = JSON.stringify(jsonObj, null, 4);
+
+ navigator.clipboard.writeText(formattedJson).then(function() {
+ const copyStatus = document.getElementById('copyStatus');
+ copyStatus.classList.add('show');
+ setTimeout(function() {
+ copyStatus.classList.remove('show');
+ }, 2000);
+ }).catch(function(err) {
+ console.error('Failed to copy to clipboard:', err);
+ alert('Failed to copy to clipboard. Please select and copy manually.');
+ });
+ } catch (e) {
+ // Fallback to original content
+ navigator.clipboard.writeText(caseContent).then(function() {
+ const copyStatus = document.getElementById('copyStatus');
+ copyStatus.classList.add('show');
+ setTimeout(function() {
+ copyStatus.classList.remove('show');
+ }, 2000);
+ }).catch(function(err) {
+ console.error('Failed to copy to clipboard:', err);
+ alert('Failed to copy to clipboard. Please select and copy manually.');
+ });
+ }
}
function downloadCaseJson() {
@@ -3050,9 +3228,11 @@ const htmlTemplate = `
try {
// Parse and format JSON for beautiful display
const jsonObj = JSON.parse(summaryContent);
- const formattedJson = JSON.stringify(jsonObj, null, 2);
+ const formattedJson = JSON.stringify(jsonObj, null, 4);
- document.getElementById('summaryContent').textContent = formattedJson;
+ // Apply syntax highlighting
+ const highlightedJson = highlightJSONAdvanced(formattedJson);
+ document.getElementById('summaryContent').innerHTML = highlightedJson;
document.getElementById('summaryModal').style.display = 'block';
} catch (e) {
console.error('Failed to parse JSON:', e);
@@ -3067,22 +3247,39 @@ const htmlTemplate = `
}
function copySummaryContent() {
- const content = document.getElementById('summaryContent').textContent;
- if (!content) {
+ // Copy the original formatted JSON content instead of highlighted HTML
+ if (!summaryContent) {
alert('No content to copy');
return;
}
- navigator.clipboard.writeText(content).then(function() {
- const copyStatus = document.getElementById('summaryStatus');
- copyStatus.classList.add('show');
- setTimeout(function() {
- copyStatus.classList.remove('show');
- }, 2000);
- }).catch(function(err) {
- console.error('Failed to copy to clipboard:', err);
- alert('Failed to copy to clipboard. Please select and copy manually.');
- });
+ try {
+ const jsonObj = JSON.parse(summaryContent);
+ const formattedJson = JSON.stringify(jsonObj, null, 4);
+
+ navigator.clipboard.writeText(formattedJson).then(function() {
+ const copyStatus = document.getElementById('summaryStatus');
+ copyStatus.classList.add('show');
+ setTimeout(function() {
+ copyStatus.classList.remove('show');
+ }, 2000);
+ }).catch(function(err) {
+ console.error('Failed to copy to clipboard:', err);
+ alert('Failed to copy to clipboard. Please select and copy manually.');
+ });
+ } catch (e) {
+ // Fallback to original content
+ navigator.clipboard.writeText(summaryContent).then(function() {
+ const copyStatus = document.getElementById('summaryStatus');
+ copyStatus.classList.add('show');
+ setTimeout(function() {
+ copyStatus.classList.remove('show');
+ }, 2000);
+ }).catch(function(err) {
+ console.error('Failed to copy to clipboard:', err);
+ alert('Failed to copy to clipboard. Please select and copy manually.');
+ });
+ }
}
// Log Content Modal functions
@@ -3141,6 +3338,19 @@ const htmlTemplate = `
if (fieldsElement.classList.contains('collapsed')) {
fieldsElement.classList.remove('collapsed');
toggleIcon.classList.add('rotated');
+ // Apply JSON highlighting when expanding
+ if (fieldsElement.classList.contains('json-inline')) {
+ const text = fieldsElement.textContent;
+ if (text && text.trim()) {
+ try {
+ JSON.parse(text);
+ const highlighted = highlightJSONAdvanced(text);
+ fieldsElement.innerHTML = highlighted;
+ } catch (e) {
+ // If not valid JSON, leave as is
+ }
+ }
+ }
} else {
fieldsElement.classList.add('collapsed');
toggleIcon.classList.remove('rotated');
@@ -3173,9 +3383,64 @@ const htmlTemplate = `
} else {
requestsContent.classList.add('show');
buttonElement.textContent = buttonElement.textContent.replace('Show', 'Hide');
+
+ // Apply JSON highlighting to request/response bodies when expanding
+ setTimeout(() => {
+ applyRequestResponseHighlighting(requestsContent);
+ }, 10);
}
}
+ // Apply JSON highlighting to request/response content
+ function applyRequestResponseHighlighting(container) {
+ // Find all request-body-compact and response-body-compact elements
+ const requestBodies = container.querySelectorAll('.request-body-compact, .response-body-compact');
+
+ requestBodies.forEach(function(element) {
+ // Skip if already processed
+ if (element.querySelector('.json-key, .json-string, .json-number')) {
+ return;
+ }
+
+ const text = element.textContent;
+ if (text && text.trim()) {
+ // Extract the content after "Request:" or "Response:"
+ const match = text.match(/^(Request|Response):\s*(.+)$/s);
+ if (match) {
+ const label = match[1];
+ const content = match[2].trim();
+ try {
+ // Validate JSON by parsing it
+ const parsedJson = JSON.parse(content);
+ // Re-stringify to get a compact, normalized string, which removes extra spaces
+ const compactJson = JSON.stringify(parsedJson);
+ // Apply highlighting on the compact string
+ const highlighted = highlightJSONAdvanced(compactJson);
+ element.innerHTML = label + ': ' + highlighted;
+ } catch (e) {
+ // If not valid JSON, leave as is
+ console.log('Not valid JSON for ' + label + ':', content);
+ }
+ } else {
+ // Try to find JSON-like content even without exact format
+ const jsonMatch = text.match(/(\{.*\}|\[.*\])/s);
+ if (jsonMatch) {
+ const jsonContent = jsonMatch[1].trim();
+ try {
+ JSON.parse(jsonContent);
+ const beforeJson = text.substring(0, text.indexOf(jsonContent));
+ const afterJson = text.substring(text.indexOf(jsonContent) + jsonContent.length);
+ const highlighted = highlightJSONAdvanced(jsonContent);
+ element.innerHTML = beforeJson + highlighted + afterJson;
+ } catch (e) {
+ // Not valid JSON, leave as is
+ }
+ }
+ }
+ }
+ });
+ }
+
function openImageModal(src) {
const modal = document.getElementById('imageModal');
const modalImg = document.getElementById('modalImage');
@@ -3208,6 +3473,25 @@ const htmlTemplate = `
}
}
+ // Apply syntax highlighting to inline JSON content
+ function applyInlineJSONHighlighting() {
+ document.querySelectorAll('.json-inline').forEach(function(element) {
+ const text = element.textContent;
+ if (text && text.trim()) {
+ try {
+ // Validate and parse JSON
+ JSON.parse(text);
+ // Apply highlighting if valid JSON
+ const highlighted = highlightJSONAdvanced(text);
+ element.innerHTML = highlighted;
+ } catch (e) {
+ // If not valid JSON, leave as is
+ // This handles cases where content might not be pure JSON
+ }
+ }
+ });
+ }
+
// Auto-expand all steps on load to show actions
document.addEventListener('DOMContentLoaded', function() {
// Expand all steps to show the actions list
@@ -3216,6 +3500,14 @@ const htmlTemplate = `
contents.forEach(content => content.classList.add('show'));
icons.forEach(icon => icon.classList.add('rotated'));
+
+ // Apply syntax highlighting to inline JSON content
+ applyInlineJSONHighlighting();
+
+ // Apply JSON highlighting to all visible request/response content
+ document.querySelectorAll('.requests-content-compact').forEach(function(container) {
+ applyRequestResponseHighlighting(container);
+ });
});
function toggleAllSteps() {
diff --git a/scripts/install-pre-commit-hook b/scripts/install-pre-commit-hook
index c26f6e54..2461890c 100644
--- a/scripts/install-pre-commit-hook
+++ b/scripts/install-pre-commit-hook
@@ -11,7 +11,7 @@ PRE_COMMIT_FILE=.git/hooks/pre-commit
# install pre-commit hook and make it executable
function install() {
go get mvdan.cc/gofumpt
- go get github.com/incu6us/goimports-reviser/v2@latest
+ go get github.com/incu6us/goimports-reviser/v3@latest
cat > $PRE_COMMIT_FILE <<'EOF'
#!/bin/bash
diff --git a/step_ui.go b/step_ui.go
index 9971ca08..c499321b 100644
--- a/step_ui.go
+++ b/step_ui.go
@@ -765,6 +765,10 @@ func runStepMobileUI(s *SessionRunner, step IStep) (stepResult *StepResult, err
identifier = action.Identifier
break
}
+ if action.Options != nil && action.Options.Identifier != "" {
+ identifier = action.Options.Identifier
+ break
+ }
}
}
stepResult.Identifier = identifier
diff --git a/uixt/driver_session.go b/uixt/driver_session.go
index 733f7440..b929a060 100644
--- a/uixt/driver_session.go
+++ b/uixt/driver_session.go
@@ -158,25 +158,42 @@ func (s *DriverSession) DELETE(urlStr string) (rawResp DriverRawResponse, err er
func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody []byte) (
rawResp DriverRawResponse, err error) {
- for count := 1; count <= s.maxRetry; count++ {
+ var lastError error
+
+ for attempt := 1; attempt <= s.maxRetry; attempt++ {
+ // Execute the request
rawResp, err = s.Request(method, urlStr, rawBody)
if err == nil {
- return
+ if attempt > 1 {
+ log.Info().Msgf("request succeeded after %d attempts", attempt)
+ }
+ return rawResp, nil
}
+
+ lastError = err
+ log.Warn().Err(err).Msgf("request failed, attempt %d/%d", attempt, s.maxRetry)
+
+ // If this was the last attempt, break
+ if attempt == s.maxRetry {
+ log.Error().Err(lastError).Msgf("all %d retry attempts failed, giving up", s.maxRetry)
+ break
+ }
+
+ // Wait before next attempt
time.Sleep(3 * time.Second)
+ // Try to reset the session for the next attempt
if s.resetFn != nil {
- log.Warn().Msg("reset driver session")
- if err2 := s.resetFn(); err2 != nil {
- log.Error().Err(err2).Msgf(
- "failed to reset session, try count %v", count)
+ log.Warn().Msgf("attempting to reset driver session before attempt %d", attempt+1)
+ if resetErr := s.resetFn(); resetErr != nil {
+ log.Error().Err(resetErr).Msgf("failed to reset session, will retry without reset")
} else {
- log.Info().Msgf(
- "reset session success, try count %v", count)
+ log.Info().Msg("session reset successful")
}
}
}
- return
+
+ return nil, lastError
}
func (s *DriverSession) Request(method string, urlStr string, rawBody []byte) (
diff --git a/uixt/ios_driver_wda.go b/uixt/ios_driver_wda.go
index f62bb0e0..23d872dc 100644
--- a/uixt/ios_driver_wda.go
+++ b/uixt/ios_driver_wda.go
@@ -36,13 +36,19 @@ func NewWDADriver(device *IOSDevice) (*WDADriver, error) {
Session: NewDriverSession(),
}
- if !device.Options.LazySetup {
- // setup driver
- if err := driver.Setup(); err != nil {
- return nil, err
- }
+ // setup driver
+ if err := driver.Setup(); err != nil {
+ return nil, err
}
+ // check WDA status
+ wdaStatus, err := driver.Status()
+ if err != nil {
+ return nil, err
+ }
+ log.Info().Interface("status", wdaStatus).
+ Msg("check WDA status")
+
// register driver session reset handler
driver.Session.RegisterResetHandler(driver.Setup)
@@ -146,13 +152,6 @@ func (wd *WDADriver) Setup() error {
return err
}
- wdaStatus, err := wd.Status()
- if err != nil {
- return err
- }
- log.Info().Interface("status", wdaStatus).
- Msg("check WDA status")
-
// create new session
if err := wd.InitSession(nil); err != nil {
return errors.Wrap(code.DeviceHTTPDriverError, err.Error())
diff --git a/uixt/ios_test.go b/uixt/ios_test.go
index 155ce703..55283f3f 100644
--- a/uixt/ios_test.go
+++ b/uixt/ios_test.go
@@ -38,8 +38,7 @@ func TestDevice_IOS_Install(t *testing.T) {
func TestDriver_WDA_LazySetup(t *testing.T) {
device, err := NewIOSDevice(
option.WithWDAPort(8700),
- option.WithWDAMjpegPort(8800),
- option.WithLazySetup(true))
+ option.WithWDAMjpegPort(8800))
require.Nil(t, err)
driver, err := NewWDADriver(device)
require.Nil(t, err)
diff --git a/uixt/mcp_server.go b/uixt/mcp_server.go
index 098fdb44..2c721fff 100644
--- a/uixt/mcp_server.go
+++ b/uixt/mcp_server.go
@@ -193,6 +193,8 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume
"tap_random_rect": tempOptions.TapRandomRect,
"anti_risk": tempOptions.AntiRisk,
"pre_mark_operation": tempOptions.PreMarkOperation,
+ "reset_history": tempOptions.ResetHistory,
+ "match_one": tempOptions.MatchOne,
}
// Add boolean options only if they are true
@@ -209,6 +211,18 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume
if tempOptions.Index != 0 {
arguments["index"] = tempOptions.Index
}
+ if tempOptions.Interval > 0 {
+ arguments["interval"] = tempOptions.Interval
+ }
+ if tempOptions.Steps > 0 {
+ arguments["steps"] = tempOptions.Steps
+ }
+ if tempOptions.Timeout > 0 {
+ arguments["timeout"] = tempOptions.Timeout
+ }
+ if tempOptions.Frequency > 0 {
+ arguments["frequency"] = tempOptions.Frequency
+ }
// Only set duration if it's not already set (to avoid overriding tool-specific conversions)
if tempOptions.Duration > 0 {
if _, exists := arguments["duration"]; !exists {
@@ -288,13 +302,19 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume
if tempOptions.Selector != "" {
arguments["selector"] = tempOptions.Selector
}
-}
-
-func getFloat64ValueOrDefault(value float64, defaultValue float64) float64 {
- if value == 0 {
- return defaultValue
+ if tempOptions.Identifier != "" {
+ arguments["identifier"] = tempOptions.Identifier
+ }
+
+ // Add direction option (can be string or []float64)
+ if tempOptions.Direction != nil {
+ arguments["direction"] = tempOptions.Direction
+ }
+
+ // Add custom options
+ if len(tempOptions.Custom) > 0 {
+ arguments["custom"] = tempOptions.Custom
}
- return value
}
// parseActionOptions converts MCP request arguments to ActionOptions struct
diff --git a/uixt/mcp_tools_ai.go b/uixt/mcp_tools_ai.go
index c932f43f..7acf31e9 100644
--- a/uixt/mcp_tools_ai.go
+++ b/uixt/mcp_tools_ai.go
@@ -4,10 +4,11 @@ import (
"context"
"fmt"
- "github.com/httprunner/httprunner/v5/uixt/option"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/rs/zerolog/log"
+
+ "github.com/httprunner/httprunner/v5/uixt/option"
)
// ToolStartToGoal implements the start_to_goal tool call.
@@ -162,7 +163,7 @@ func (t *ToolAIQuery) Implement() server.ToolHandlerFunc {
return nil, err
}
- // Build action options from unified request
+ // Build all options from request arguments
opts := unifiedReq.Options()
// AI query logic with options
diff --git a/uixt/mcp_tools_swipe.go b/uixt/mcp_tools_swipe.go
index 1dfa6dd7..d624da7f 100644
--- a/uixt/mcp_tools_swipe.go
+++ b/uixt/mcp_tools_swipe.go
@@ -5,11 +5,12 @@ import (
"fmt"
"slices"
- "github.com/httprunner/httprunner/v5/internal/builtin"
- "github.com/httprunner/httprunner/v5/uixt/option"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/rs/zerolog/log"
+
+ "github.com/httprunner/httprunner/v5/internal/builtin"
+ "github.com/httprunner/httprunner/v5/uixt/option"
)
// ToolSwipe implements the generic swipe tool call.
@@ -124,15 +125,13 @@ func (t *ToolSwipeDirection) Implement() server.ToolHandlerFunc {
swipeDirection, validDirections)
}
- opts := []option.ActionOption{
- option.WithDuration(getFloat64ValueOrDefault(unifiedReq.Duration, 0.5)),
- option.WithPressDuration(getFloat64ValueOrDefault(unifiedReq.PressDuration, 0.1)),
+ // Build all options from request arguments
+ opts := unifiedReq.Options()
+ if unifiedReq.Duration == 0 {
+ opts = append(opts, option.WithDuration(0.5))
}
- if unifiedReq.AntiRisk {
- opts = append(opts, option.WithAntiRisk(true))
- }
- if unifiedReq.PreMarkOperation {
- opts = append(opts, option.WithPreMarkOperation(true))
+ if unifiedReq.PressDuration == 0 {
+ opts = append(opts, option.WithPressDuration(0.1))
}
// Convert direction to coordinates and perform swipe
@@ -240,17 +239,8 @@ func (t *ToolSwipeCoordinate) Implement() server.ToolHandlerFunc {
params := []float64{unifiedReq.FromX, unifiedReq.FromY, unifiedReq.ToX, unifiedReq.ToY}
- // Build action options from the unified request
- opts := []option.ActionOption{}
- if unifiedReq.Duration > 0 {
- opts = append(opts, option.WithDuration(unifiedReq.Duration))
- }
- if unifiedReq.PressDuration > 0 {
- opts = append(opts, option.WithPressDuration(unifiedReq.PressDuration))
- }
- if unifiedReq.AntiRisk {
- opts = append(opts, option.WithAntiRisk(true))
- }
+ // Build all options from request arguments
+ opts := unifiedReq.Options()
swipeAction := prepareSwipeAction(driverExt, params, opts...)
err = swipeAction(driverExt)
@@ -327,7 +317,7 @@ func (t *ToolSwipeToTapApp) Implement() server.ToolHandlerFunc {
}
// Build action options from request structure
- var opts []option.ActionOption
+ opts := unifiedReq.Options()
// Add boolean options
if unifiedReq.IgnoreNotFoundError {
@@ -400,24 +390,8 @@ func (t *ToolSwipeToTapText) Implement() server.ToolHandlerFunc {
return nil, err
}
- // Build action options from request structure
- var opts []option.ActionOption
-
- // Add boolean options
- if unifiedReq.IgnoreNotFoundError {
- opts = append(opts, option.WithIgnoreNotFoundError(true))
- }
- if unifiedReq.Regex {
- opts = append(opts, option.WithRegex(true))
- }
-
- // Add numeric options
- if unifiedReq.MaxRetryTimes > 0 {
- opts = append(opts, option.WithMaxRetryTimes(unifiedReq.MaxRetryTimes))
- }
- if unifiedReq.Index > 0 {
- opts = append(opts, option.WithIndex(unifiedReq.Index))
- }
+ // Build all options from request arguments
+ opts := unifiedReq.Options()
// Swipe to tap text action logic
err = driverExt.SwipeToTapTexts([]string{unifiedReq.Text}, opts...)
@@ -478,24 +452,8 @@ func (t *ToolSwipeToTapTexts) Implement() server.ToolHandlerFunc {
return nil, err
}
- // Build action options from request structure
- var opts []option.ActionOption
-
- // Add boolean options
- if unifiedReq.IgnoreNotFoundError {
- opts = append(opts, option.WithIgnoreNotFoundError(true))
- }
- if unifiedReq.Regex {
- opts = append(opts, option.WithRegex(true))
- }
-
- // Add numeric options
- if unifiedReq.MaxRetryTimes > 0 {
- opts = append(opts, option.WithMaxRetryTimes(unifiedReq.MaxRetryTimes))
- }
- if unifiedReq.Index > 0 {
- opts = append(opts, option.WithIndex(unifiedReq.Index))
- }
+ // Build all options from request arguments
+ opts := unifiedReq.Options()
// Swipe to tap texts action logic
err = driverExt.SwipeToTapTexts(unifiedReq.Texts, opts...)
@@ -575,12 +533,10 @@ func (t *ToolDrag) Implement() server.ToolHandlerFunc {
return nil, fmt.Errorf("from_x, from_y, to_x, and to_y coordinates are required")
}
- opts := []option.ActionOption{}
- if unifiedReq.Duration > 0 {
- opts = append(opts, option.WithDuration(unifiedReq.Duration/1000.0))
- }
- if unifiedReq.AntiRisk {
- opts = append(opts, option.WithAntiRisk(true))
+ // Build all options from request arguments
+ opts := unifiedReq.Options()
+ if unifiedReq.Duration == 0 {
+ opts = append(opts, option.WithDuration(0.5))
}
// Drag action logic
diff --git a/uixt/mcp_tools_touch.go b/uixt/mcp_tools_touch.go
index 19e42261..539c7bb4 100644
--- a/uixt/mcp_tools_touch.go
+++ b/uixt/mcp_tools_touch.go
@@ -4,10 +4,11 @@ import (
"context"
"fmt"
- "github.com/httprunner/httprunner/v5/internal/builtin"
- "github.com/httprunner/httprunner/v5/uixt/option"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
+
+ "github.com/httprunner/httprunner/v5/internal/builtin"
+ "github.com/httprunner/httprunner/v5/uixt/option"
)
// ToolTapXY implements the tap_xy tool call.
@@ -42,14 +43,9 @@ func (t *ToolTapXY) Implement() server.ToolHandlerFunc {
return nil, err
}
- // Get options directly since ActionOptions is now ActionOptions
+ // Build all options from request arguments
opts := unifiedReq.Options()
- // Add configurable options based on request
- if unifiedReq.PreMarkOperation {
- opts = append(opts, option.WithPreMarkOperation(true))
- }
-
// Validate required parameters
if unifiedReq.X == 0 || unifiedReq.Y == 0 {
return nil, fmt.Errorf("x and y coordinates are required")
@@ -123,19 +119,9 @@ func (t *ToolTapAbsXY) Implement() server.ToolHandlerFunc {
return nil, err
}
- // Get options directly since ActionOptions is now ActionOptions
+ // Build all options from request arguments
opts := unifiedReq.Options()
- // Add configurable options based on request
- if unifiedReq.PreMarkOperation {
- opts = append(opts, option.WithPreMarkOperation(true))
- }
-
- // Add AntiRisk support
- if unifiedReq.AntiRisk {
- opts = append(opts, option.WithAntiRisk(true))
- }
-
// Validate required parameters
if unifiedReq.X == 0 || unifiedReq.Y == 0 {
return nil, fmt.Errorf("x and y coordinates are required")
@@ -208,14 +194,9 @@ func (t *ToolTapByOCR) Implement() server.ToolHandlerFunc {
return nil, err
}
- // Get options directly since ActionOptions is now ActionOptions
+ // Build all options from request arguments
opts := unifiedReq.Options()
- // Add configurable options based on request
- if unifiedReq.PreMarkOperation {
- opts = append(opts, option.WithPreMarkOperation(true))
- }
-
// Validate required parameters
if unifiedReq.Text == "" {
return nil, fmt.Errorf("text parameter is required")
@@ -277,14 +258,9 @@ func (t *ToolTapByCV) Implement() server.ToolHandlerFunc {
return nil, err
}
- // Get options directly since ActionOptions is now ActionOptions
+ // Build all options from request arguments
opts := unifiedReq.Options()
- // Add configurable options based on request
- if unifiedReq.PreMarkOperation {
- opts = append(opts, option.WithPreMarkOperation(true))
- }
-
// For TapByCV, we need to check if there are UI types in the options
// In the original DoAction, it requires ScreenShotWithUITypes to be set
// We'll add a basic implementation that triggers CV recognition
diff --git a/uixt/mcp_tools_web.go b/uixt/mcp_tools_web.go
index a21a7489..a34c416b 100644
--- a/uixt/mcp_tools_web.go
+++ b/uixt/mcp_tools_web.go
@@ -5,11 +5,12 @@ import (
"encoding/json"
"fmt"
- "github.com/httprunner/httprunner/v5/internal/builtin"
- "github.com/httprunner/httprunner/v5/uixt/option"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/rs/zerolog/log"
+
+ "github.com/httprunner/httprunner/v5/internal/builtin"
+ "github.com/httprunner/httprunner/v5/uixt/option"
)
// ToolWebLoginNoneUI implements the web_login_none_ui tool call.
@@ -170,7 +171,7 @@ func (t *ToolHoverBySelector) Implement() server.ToolHandlerFunc {
if err != nil {
return nil, err
}
- // Get options directly since ActionOptions is now ActionOptions
+ // Build all options from request arguments
opts := unifiedReq.Options()
// Hover by selector action logic
@@ -228,7 +229,7 @@ func (t *ToolTapBySelector) Implement() server.ToolHandlerFunc {
if err != nil {
return nil, err
}
- // Get options directly since ActionOptions is now ActionOptions
+ // Build all options from request arguments
opts := unifiedReq.Options()
// Tap by selector action logic
@@ -286,7 +287,7 @@ func (t *ToolSecondaryClickBySelector) Implement() server.ToolHandlerFunc {
if err != nil {
return nil, err
}
- // Get options directly since ActionOptions is now ActionOptions
+ // Build all options from request arguments
opts := unifiedReq.Options()
// Secondary click by selector action logic
diff --git a/uixt/option/action.go b/uixt/option/action.go
index b9f45b0c..f811562e 100644
--- a/uixt/option/action.go
+++ b/uixt/option/action.go
@@ -7,10 +7,11 @@ import (
"reflect"
"strings"
- "github.com/httprunner/httprunner/v5/internal/builtin"
- "github.com/httprunner/httprunner/v5/uixt/types"
"github.com/mark3labs/mcp-go/mcp"
"github.com/rs/zerolog/log"
+
+ "github.com/httprunner/httprunner/v5/internal/builtin"
+ "github.com/httprunner/httprunner/v5/uixt/types"
)
type MobileAction struct {
@@ -326,6 +327,10 @@ func (o *ActionOptions) Options() []ActionOption {
options = append(options, WithAntiRisk(true))
}
+ if o.PreMarkOperation {
+ options = append(options, WithPreMarkOperation(true))
+ }
+
// custom options
if o.Custom != nil {
for k, v := range o.Custom {
diff --git a/uixt/option/device.go b/uixt/option/device.go
index 1b1257ae..c40b300c 100644
--- a/uixt/option/device.go
+++ b/uixt/option/device.go
@@ -152,17 +152,6 @@ func WithDeviceWDAMjpegPort(port int) DeviceOption {
}
}
-func WithDeviceLazySetup(lazySetup bool) DeviceOption {
- return func(device *DeviceOptions) {
- if device.IOSDeviceOptions != nil {
- device.IOSDeviceOptions.LazySetup = lazySetup
- }
- if device.Platform == "" {
- device.Platform = "ios"
- }
- }
-}
-
func WithDeviceResetHomeOnStartup(reset bool) DeviceOption {
return func(device *DeviceOptions) {
if device.IOSDeviceOptions != nil {
diff --git a/uixt/option/ios.go b/uixt/option/ios.go
index 42b9b2bd..a19c716d 100644
--- a/uixt/option/ios.go
+++ b/uixt/option/ios.go
@@ -6,7 +6,6 @@ type IOSDeviceOptions struct {
WDAPort int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port
WDAMjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
- LazySetup bool `json:"lazy_setup,omitempty" yaml:"lazy_setup,omitempty"` // lazy setup WDA
// switch to iOS springboard before init WDA session
ResetHomeOnStartup bool `json:"reset_home_on_startup,omitempty" yaml:"reset_home_on_startup,omitempty"`
@@ -33,9 +32,6 @@ func (dev *IOSDeviceOptions) Options() (deviceOptions []IOSDeviceOption) {
if dev.LogOn {
deviceOptions = append(deviceOptions, WithWDALogOn(true))
}
- if dev.LazySetup {
- deviceOptions = append(deviceOptions, WithLazySetup(true))
- }
if dev.ResetHomeOnStartup {
deviceOptions = append(deviceOptions, WithResetHomeOnStartup(true))
}
@@ -104,12 +100,6 @@ func WithWDALogOn(logOn bool) IOSDeviceOption {
}
}
-func WithLazySetup(lazySetup bool) IOSDeviceOption {
- return func(device *IOSDeviceOptions) {
- device.LazySetup = lazySetup
- }
-}
-
func WithResetHomeOnStartup(reset bool) IOSDeviceOption {
return func(device *IOSDeviceOptions) {
device.ResetHomeOnStartup = reset