mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-04 23:16:57 +08:00
feat: add horizontal scrolling screenshot display and improve screenshot handling in report generation
This commit is contained in:
@@ -1232,6 +1232,113 @@ const htmlTemplate = `<!DOCTYPE html>
|
|||||||
transform: scale(1.02);
|
transform: scale(1.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Horizontal scrolling screenshot styles */
|
||||||
|
.screenshot-horizontal-scroll {
|
||||||
|
display: flex;
|
||||||
|
gap: 0 !important;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
padding: 8px;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 0;
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-horizontal-scroll::-webkit-scrollbar {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-horizontal-scroll::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-horizontal-scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-horizontal-scroll::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-item-horizontal {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 180px;
|
||||||
|
max-width: 280px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
outline: none;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-item-horizontal .screenshot-image {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 250px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-item-horizontal .screenshot-image img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
object-fit: contain;
|
||||||
|
box-shadow: none;
|
||||||
|
display: block;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
vertical-align: top;
|
||||||
|
float: left;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-item-horizontal .screenshot-image img:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Direct inline screenshot styles */
|
||||||
|
.screenshot-inline {
|
||||||
|
max-height: 250px;
|
||||||
|
object-fit: contain;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 4px 0 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
vertical-align: top;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-inline:last-child {
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-inline:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
.actions-details {
|
.actions-details {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
@@ -2542,19 +2649,30 @@ const htmlTemplate = `<!DOCTYPE html>
|
|||||||
<span class="step-name">📸 Take Screenshot</span>
|
<span class="step-name">📸 Take Screenshot</span>
|
||||||
<span class="duration">{{formatDuration $planning.ScreenshotElapsed}}</span>
|
<span class="duration">{{formatDuration $planning.ScreenshotElapsed}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{if $planning.ScreenResult}}
|
<div class="screenshot-display screenshot-horizontal-scroll">
|
||||||
<div class="screenshot-display">
|
{{if $planning.ScreenResult}}
|
||||||
{{$screenshot := $planning.ScreenResult}}
|
{{if $planning.ScreenResult.ImagePath}}
|
||||||
{{$base64Image := encodeImageBase64 $screenshot.ImagePath}}
|
{{$base64Image := encodeImageBase64 $planning.ScreenResult.ImagePath}}
|
||||||
{{if $base64Image}}
|
{{if $base64Image}}
|
||||||
<div class="screenshot-item-compact">
|
<img src="data:image/jpeg;base64,{{$base64Image}}" alt="Planning Screenshot" onclick="openImageModal(this.src)" class="screenshot-inline" />
|
||||||
<div class="screenshot-image">
|
{{end}}
|
||||||
<img src="data:image/jpeg;base64,{{$base64Image}}" alt="Planning Screenshot" onclick="openImageModal(this.src)" />
|
{{end}}
|
||||||
</div>
|
{{end}}
|
||||||
</div>
|
{{if $planning.SubActions}}
|
||||||
|
{{range $subAction := $planning.SubActions}}
|
||||||
|
{{if $subAction.ScreenResults}}
|
||||||
|
{{range $subScreenshot := $subAction.ScreenResults}}
|
||||||
|
{{if $subScreenshot.ImagePath}}
|
||||||
|
{{$base64Image := encodeImageBase64 $subScreenshot.ImagePath}}
|
||||||
|
{{if $base64Image}}
|
||||||
|
<img src="data:image/jpeg;base64,{{$base64Image}}" alt="Sub-action Screenshot" onclick="openImageModal(this.src)" class="screenshot-inline" />
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/httprunner/httprunner/v5/uixt/types"
|
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ScreenResult represents the result of taking a screenshot, including image path, recognition results, and metadata
|
||||||
type ScreenResult struct {
|
type ScreenResult struct {
|
||||||
bufSource *bytes.Buffer // raw image buffer bytes
|
bufSource *bytes.Buffer // raw image buffer bytes
|
||||||
ImagePath string `json:"image_path"` // image file path
|
ImagePath string `json:"image_path"` // image file path
|
||||||
@@ -467,7 +468,7 @@ func MarkUIOperation(driver IDriver, actionType option.ActionName, actionCoordin
|
|||||||
timestamp := builtin.GenNameWithTimestamp("%d")
|
timestamp := builtin.GenNameWithTimestamp("%d")
|
||||||
imagePath := filepath.Join(
|
imagePath := filepath.Join(
|
||||||
config.GetConfig().ScreenShotsPath(),
|
config.GetConfig().ScreenShotsPath(),
|
||||||
fmt.Sprintf("action_%s_pre_%s.png", timestamp, actionType),
|
fmt.Sprintf("%s_pre_mark_%s.png", timestamp, actionType),
|
||||||
)
|
)
|
||||||
|
|
||||||
switch actionType {
|
switch actionType {
|
||||||
|
|||||||
+13
-11
@@ -6,16 +6,17 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"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/uixt/ai"
|
"github.com/httprunner/httprunner/v5/uixt/ai"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func preHandler_TapAbsXY(driver IDriver, options *option.ActionOptions, rawX, rawY float64) (
|
func preHandler_TapAbsXY(driver IDriver, options *option.ActionOptions, rawX, rawY float64) (
|
||||||
x, y float64, err error) {
|
x, y float64, err error,
|
||||||
|
) {
|
||||||
// Call MCP action tool if anti-risk is enabled
|
// Call MCP action tool if anti-risk is enabled
|
||||||
if options.AntiRisk {
|
if options.AntiRisk {
|
||||||
arguments := getAntiRisk_SetTouchInfoList_Arguments(driver, []ai.PointF{
|
arguments := getAntiRisk_SetTouchInfoList_Arguments(driver, []ai.PointF{
|
||||||
@@ -40,8 +41,8 @@ func preHandler_TapAbsXY(driver IDriver, options *option.ActionOptions, rawX, ra
|
|||||||
}
|
}
|
||||||
|
|
||||||
func preHandler_DoubleTap(driver IDriver, options *option.ActionOptions, rawX, rawY float64) (
|
func preHandler_DoubleTap(driver IDriver, options *option.ActionOptions, rawX, rawY float64) (
|
||||||
x, y float64, err error) {
|
x, y float64, err error,
|
||||||
|
) {
|
||||||
x, y, err = convertToAbsolutePoint(driver, rawX, rawY)
|
x, y, err = convertToAbsolutePoint(driver, rawX, rawY)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
@@ -60,8 +61,8 @@ func preHandler_DoubleTap(driver IDriver, options *option.ActionOptions, rawX, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
func preHandler_Drag(driver IDriver, options *option.ActionOptions, rawFomX, rawFromY, rawToX, rawToY float64) (
|
func preHandler_Drag(driver IDriver, options *option.ActionOptions, rawFomX, rawFromY, rawToX, rawToY float64) (
|
||||||
fromX, fromY, toX, toY float64, err error) {
|
fromX, fromY, toX, toY float64, err error,
|
||||||
|
) {
|
||||||
fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(driver, rawFomX, rawFromY, rawToX, rawToY)
|
fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(driver, rawFomX, rawFromY, rawToX, rawToY)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, 0, 0, err
|
return 0, 0, 0, 0, err
|
||||||
@@ -92,8 +93,8 @@ func preHandler_Drag(driver IDriver, options *option.ActionOptions, rawFomX, raw
|
|||||||
|
|
||||||
func preHandler_Swipe(driver IDriver, actionType option.ActionName,
|
func preHandler_Swipe(driver IDriver, actionType option.ActionName,
|
||||||
options *option.ActionOptions, rawFomX, rawFromY, rawToX, rawToY float64) (
|
options *option.ActionOptions, rawFomX, rawFromY, rawToX, rawToY float64) (
|
||||||
fromX, fromY, toX, toY float64, err error) {
|
fromX, fromY, toX, toY float64, err error,
|
||||||
|
) {
|
||||||
fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(driver, rawFomX, rawFromY, rawToX, rawToY)
|
fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(driver, rawFomX, rawFromY, rawToX, rawToY)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, 0, 0, err
|
return 0, 0, 0, 0, err
|
||||||
@@ -142,7 +143,7 @@ func postHandler(driver IDriver, actionType option.ActionName, options *option.A
|
|||||||
timestamp := builtin.GenNameWithTimestamp("%d")
|
timestamp := builtin.GenNameWithTimestamp("%d")
|
||||||
imagePath := filepath.Join(
|
imagePath := filepath.Join(
|
||||||
config.GetConfig().ScreenShotsPath(),
|
config.GetConfig().ScreenShotsPath(),
|
||||||
fmt.Sprintf("action_%s_post_%s.png", timestamp, actionType),
|
fmt.Sprintf("%s_post_mark_%s.png", timestamp, actionType),
|
||||||
)
|
)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -157,7 +158,8 @@ func postHandler(driver IDriver, actionType option.ActionName, options *option.A
|
|||||||
|
|
||||||
// callMCPActionTool calls MCP tool for the given action
|
// callMCPActionTool calls MCP tool for the given action
|
||||||
func callMCPActionTool(driver IDriver,
|
func callMCPActionTool(driver IDriver,
|
||||||
serverName, actionType string, arguments map[string]any) {
|
serverName, actionType string, arguments map[string]any,
|
||||||
|
) {
|
||||||
// Get XTDriver from cache
|
// Get XTDriver from cache
|
||||||
dExt := getXTDriverFromCache(driver)
|
dExt := getXTDriverFromCache(driver)
|
||||||
if dExt == nil {
|
if dExt == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user