fix: display ai assert in html report

This commit is contained in:
lilong.129
2025-07-06 11:08:52 +08:00
parent af40d082f7
commit d329fb610f
6 changed files with 183 additions and 17 deletions

149
report.go
View File

@@ -1838,6 +1838,101 @@ const htmlTemplate = `<!DOCTYPE html>
word-wrap: break-word; word-wrap: break-word;
} }
/* AI Assertion Styles */
.ai-assertion-section {
margin-top: 15px;
padding: 15px;
background: linear-gradient(135deg, #f0f8ff 0%, #f5f5ff 100%);
border: 2px solid #4169e1;
border-radius: 12px;
box-shadow: 0 4px 8px rgba(65, 105, 225, 0.15);
}
.ai-assertion-section h5 {
margin: 0 0 15px 0;
color: #4169e1;
font-size: 1.1em;
font-weight: 600;
}
.ai-screenshot-container {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 12px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.ai-screenshot {
text-align: center;
margin-top: 10px;
}
.ai-screenshot img {
max-width: 100%;
height: auto;
border-radius: 8px;
border: 1px solid #dee2e6;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s;
}
.ai-screenshot img:hover {
transform: scale(1.02);
}
.ai-analysis-container {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.ai-analysis-content {
margin-top: 10px;
}
.ai-thought {
background: linear-gradient(135deg, #e8f4fd 0%, #f0f8ff 100%);
border: 1px solid #4169e1;
border-radius: 8px;
padding: 12px;
margin: 10px 0;
color: #2c3e50;
}
.ai-thought .thought-content {
margin-top: 8px;
font-style: italic;
color: #34495e;
white-space: pre-wrap;
word-wrap: break-word;
}
.ai-raw-response {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 12px;
margin: 10px 0;
color: #2c3e50;
}
.ai-raw-response .response-content {
margin-top: 8px;
font-family: monospace;
font-size: 0.9em;
background: white;
padding: 8px;
border-radius: 4px;
border: 1px solid #e9ecef;
white-space: pre-wrap;
word-wrap: break-word;
}
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.validator-ai-layout { .validator-ai-layout {
flex-direction: column; flex-direction: column;
@@ -2950,6 +3045,60 @@ const htmlTemplate = `<!DOCTYPE html>
{{if and $validator.msg (ne $validator.check_result "pass")}} {{if and $validator.msg (ne $validator.check_result "pass")}}
<div class="validator-message">{{$validator.msg}}</div> <div class="validator-message">{{$validator.msg}}</div>
{{end}} {{end}}
<!-- AI Assertion Results -->
{{if $validator.ai_result}}
<div class="ai-assertion-section">
<h5>🤖 AI Assertion Details</h5>
<!-- AI Assertion Screenshot -->
{{if $validator.ai_result.image_path}}
<div class="ai-screenshot-container">
<span class="step-name">📸 AI Assertion Screenshot</span>
{{if $validator.ai_result.screenshot_elapsed}}
<span class="duration">{{formatDuration $validator.ai_result.screenshot_elapsed}}</span>
{{end}}
<div class="ai-screenshot">
{{$base64Image := encodeImageBase64 $validator.ai_result.image_path}}
{{if $base64Image}}
<img src="data:image/jpeg;base64,{{$base64Image}}" alt="AI Assertion Screenshot" onclick="openImageModal(this.src)" />
{{end}}
</div>
</div>
{{end}}
<!-- AI Model Analysis -->
<div class="ai-analysis-container">
<span class="step-name">🧠 AI Model Analysis</span>
{{if $validator.ai_result.model_call_elapsed}}
<span class="duration">{{formatDuration $validator.ai_result.model_call_elapsed}}</span>
{{end}}
<div class="ai-analysis-content">
{{if $validator.ai_result.assertion_result.model_name}}
<div class="model-info">🤖 Model: {{$validator.ai_result.assertion_result.model_name}}</div>
{{end}}
{{if $validator.ai_result.assertion_result.usage}}
<div class="usage-info">📊 Tokens: {{$validator.ai_result.assertion_result.usage.PromptTokens}} in / {{$validator.ai_result.assertion_result.usage.CompletionTokens}} out / {{$validator.ai_result.assertion_result.usage.TotalTokens}} total</div>
{{end}}
{{if $validator.ai_result.resolution}}
<div class="model-info">📐 Resolution: {{$validator.ai_result.resolution.Width}}x{{$validator.ai_result.resolution.Height}}</div>
{{end}}
{{if $validator.ai_result.assertion_result.thought}}
<div class="ai-thought">
<strong>💭 AI Reasoning:</strong>
<div class="thought-content">{{$validator.ai_result.assertion_result.thought}}</div>
</div>
{{end}}
{{if $validator.ai_result.assertion_result.content}}
<div class="ai-raw-response">
<strong>📝 Raw Model Response:</strong>
<div class="response-content">{{$validator.ai_result.assertion_result.content}}</div>
</div>
{{end}}
</div>
</div>
</div>
{{end}}
</div> </div>
{{end}} {{end}}
</div> </div>

View File

@@ -1042,7 +1042,8 @@ func validateUI(ud *uixt.XTDriver, iValidators []interface{}, parser *Parser, st
} }
// Perform validation // Perform validation
err = ud.DoValidation(validator.Check, validator.Assert, expected, validator.Message) validationResult.AIResult, err = ud.DoValidation(
validator.Check, validator.Assert, expected, validator.Message)
if err != nil { if err != nil {
// Add the failed validation result to the list before returning error // Add the failed validation result to the list before returning error
validateResults = append(validateResults, validationResult) validateResults = append(validateResults, validationResult)

View File

@@ -11,6 +11,7 @@ import (
"github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/internal/config" "github.com/httprunner/httprunner/v5/internal/config"
"github.com/httprunner/httprunner/v5/internal/version" "github.com/httprunner/httprunner/v5/internal/version"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/httprunner/httprunner/v5/uixt/option" "github.com/httprunner/httprunner/v5/uixt/option"
) )
@@ -233,6 +234,7 @@ type Address struct {
type ValidationResult struct { type ValidationResult struct {
Validator Validator
CheckValue interface{} `json:"check_value" yaml:"check_value"` CheckValue interface{} `json:"check_value" yaml:"check_value"`
CheckResult string `json:"check_result" yaml:"check_result"` CheckResult string `json:"check_result" yaml:"check_result"`
AIResult *uixt.AIExecutionResult `json:"ai_result,omitempty" yaml:"ai_result,omitempty"` // store AI assertion result for displaying in report
} }

View File

@@ -33,8 +33,9 @@ type AssertOptions struct {
type AssertionResult struct { type AssertionResult struct {
Pass bool `json:"pass"` Pass bool `json:"pass"`
Thought string `json:"thought"` Thought string `json:"thought"`
ModelName string `json:"model_name"` // model name used for assertion Content string `json:"content,omitempty"` // raw response content
Usage *schema.TokenUsage `json:"usage,omitempty"` // token usage statistics ModelName string `json:"model_name"` // model name used for assertion
Usage *schema.TokenUsage `json:"usage,omitempty"` // token usage statistics
} }
// Asserter handles assertion using different AI models // Asserter handles assertion using different AI models
@@ -180,5 +181,6 @@ func parseAssertionResult(content string, modelType option.LLMServiceType) (*Ass
} }
result.ModelName = string(modelType) result.ModelName = string(modelType)
result.Content = content // Store the original response content
return &result, nil return &result, nil
} }

View File

@@ -492,9 +492,10 @@ func (dExt *XTDriver) AIAssert(assertion string, opts ...option.ActionOption) (*
return assertResult, errors.Wrap(err, "AI assertion failed") return assertResult, errors.Wrap(err, "AI assertion failed")
} }
// For assertion failure, we should still return success but mark the assertion as failed
// This ensures that the AIResult (including screenshot and thought) is properly saved and displayed
if !result.Pass { if !result.Pass {
assertResult.Error = result.Thought assertResult.Error = result.Thought // Store the failure reason for reporting
return assertResult, errors.New(result.Thought)
} }
return assertResult, nil return assertResult, nil

View File

@@ -65,8 +65,8 @@ func convertToAbsolutePoint(driver IDriver, x, y float64) (absX, absY float64, e
} }
func convertToAbsoluteCoordinates(driver IDriver, fromX, fromY, toX, toY float64) ( func convertToAbsoluteCoordinates(driver IDriver, fromX, fromY, toX, toY float64) (
absFromX, absFromY, absToX, absToY float64, err error) { absFromX, absFromY, absToX, absToY float64, err error,
) {
// absolute coordinates // absolute coordinates
if fromX > 1 || toX > 1 || fromY > 1 || toY > 1 { if fromX > 1 || toX > 1 || fromY > 1 || toY > 1 {
return fromX, fromY, toX, toY, nil return fromX, fromY, toX, toY, nil
@@ -190,32 +190,43 @@ func (dExt *XTDriver) assertSelector(selector, assert string) error {
return nil return nil
} }
func (dExt *XTDriver) DoValidation(check, assert, expected string, message ...string) (err error) { func (dExt *XTDriver) DoValidation(check, assert, expected string, message ...string) (aiResult *AIExecutionResult, err error) {
switch check { switch check {
case option.SelectorOCR: case option.SelectorOCR:
err = dExt.assertOCR(expected, assert) err = dExt.assertOCR(expected, assert)
case option.SelectorAI: case option.SelectorAI:
_, err = dExt.AIAssert(expected) aiResult, err = dExt.AIAssert(expected)
case option.SelectorForegroundApp: case option.SelectorForegroundApp:
err = dExt.assertForegroundApp(expected, assert) err = dExt.assertForegroundApp(expected, assert)
case option.SelectorSelector: case option.SelectorSelector:
err = dExt.assertSelector(expected, assert) err = dExt.assertSelector(expected, assert)
default: default:
return fmt.Errorf("validator %s not implemented", check) return nil, fmt.Errorf("validator %s not implemented", check)
} }
if err != nil { if err != nil {
// Technical error (not assertion failure)
if message == nil { if message == nil {
message = []string{""} message = []string{""}
} }
log.Error().Err(err).Str("assert", assert).Str("expect", expected). log.Error().Err(err).Str("assert", assert).Str("expect", expected).
Str("msg", message[0]).Msg("validate failed") Str("msg", message[0]).Msg("validate failed")
return err return nil, err
} else if aiResult != nil {
// Check assertion result instead of relying on error
if !aiResult.AssertionResult.Pass {
return aiResult, errors.New(aiResult.AssertionResult.Thought)
}
log.Info().Str("check", check).Str("assert", assert).
Str("expect", expected).
Interface("ai_assertion_result", aiResult.AssertionResult).
Msg("ai assertion passed")
return aiResult, nil
} else {
log.Info().Str("check", check).Str("assert", assert).
Str("expect", expected).Msg("validate success")
return nil, nil
} }
log.Info().Str("check", check).Str("assert", assert).
Str("expect", expected).Msg("validate success")
return nil
} }
type SleepConfig struct { type SleepConfig struct {