diff --git a/pkg/flow/flow.go b/pkg/flow/flow.go index 3b16f5b..2f99e8a 100644 --- a/pkg/flow/flow.go +++ b/pkg/flow/flow.go @@ -69,6 +69,7 @@ type Config struct { Name string `yaml:"name"` Tags []string `yaml:"tags"` Env map[string]string `yaml:"env"` + Properties map[string]string `yaml:"properties"` Timeout int `yaml:"timeout"` // Flow timeout in ms CommandTimeout int `yaml:"commandTimeout"` // Default timeout for all commands in ms (overrides driver default) WaitForIdleTimeout *int `yaml:"waitForIdleTimeout"` // Wait for device idle in ms (nil = use global, 0 = disabled) diff --git a/pkg/report/builder.go b/pkg/report/builder.go index 39b2aba..adf3be0 100644 --- a/pkg/report/builder.go +++ b/pkg/report/builder.go @@ -62,6 +62,7 @@ func BuildSkeleton(flows []flow.Flow, cfg BuilderConfig) (*Index, []FlowDetail, Name: flowName, SourceFile: f.SourcePath, Tags: f.Config.Tags, + Properties: f.Config.Properties, Device: &cfg.Device, DataFile: filepath.Join("flows", flowID+".json"), AssetsDir: filepath.Join("assets", flowID), @@ -80,6 +81,7 @@ func BuildSkeleton(flows []flow.Flow, cfg BuilderConfig) (*Index, []FlowDetail, Name: flowName, SourceFile: f.SourcePath, Tags: f.Config.Tags, + Properties: f.Config.Properties, Device: &cfg.Device, // Device that runs this flow (for multi-device support) Commands: commands, Artifacts: FlowArtifacts{}, diff --git a/pkg/report/junit.go b/pkg/report/junit.go index df08211..49b0535 100644 --- a/pkg/report/junit.go +++ b/pkg/report/junit.go @@ -109,6 +109,15 @@ func buildTestCase(entry *FlowEntry, detail *FlowDetail, index *Index) string { )) } } + if len(entry.Properties) > 0 { + for key, value := range entry.Properties { + b.WriteString(fmt.Sprintf( + ` `+"\n", + xmlEscape(key), + xmlEscape(value), + )) + } + } b.WriteString(" \n") // Status-specific elements diff --git a/pkg/report/junit_test.go b/pkg/report/junit_test.go index 76e4297..154e039 100644 --- a/pkg/report/junit_test.go +++ b/pkg/report/junit_test.go @@ -634,3 +634,31 @@ func TestBuildJUnitXMLNoEndTime(t *testing.T) { t.Errorf("expected time=0.000 when no end time\nGot:\n%s", xml) } } + +func TestBuildJUnitXMLCustomProperties(t *testing.T) { + now := time.Now() + index := &Index{ + Version: "1.0.0", + Status: StatusPassed, + StartTime: now, + Device: Device{ID: "test", Name: "Test", Platform: "android"}, + Summary: Summary{Total: 1, Passed: 1}, + Flows: []FlowEntry{ + { + Index: 0, ID: "flow-000", Name: "Test", + SourceFile: "test.yaml", DataFile: "flows/flow-000.json", + Status: StatusPassed, Properties: map[string]string{"testID": "Test-1234"}, + }, + }, + } + + flows := []FlowDetail{ + {ID: "flow-000", Name: "Test", Commands: []Command{}}, + } + + xml := buildJUnitXML(index, flows) + + if !strings.Contains(xml, `property name="testID" value="Test-1234"`) { + t.Errorf("expected custom property\nGot:\n%s", xml) + } +} diff --git a/pkg/report/types.go b/pkg/report/types.go index 15db04e..61a8adc 100644 --- a/pkg/report/types.go +++ b/pkg/report/types.go @@ -98,24 +98,25 @@ type Summary struct { // FlowEntry is the index entry for a flow (minimal info). type FlowEntry struct { - Index int `json:"index"` // Original position - ID string `json:"id"` // Unique flow ID - Name string `json:"name"` // Display name - SourceFile string `json:"sourceFile"` // Path to YAML file - Tags []string `json:"tags,omitempty"` // Tags for filtering - Device *Device `json:"device,omitempty"` // Device that ran this flow (for multi-device runs) - DataFile string `json:"dataFile"` // Path to flow detail JSON - AssetsDir string `json:"assetsDir"` // Path to assets directory - Status Status `json:"status"` - UpdateSeq uint64 `json:"updateSeq"` - StartTime *time.Time `json:"startTime,omitempty"` - EndTime *time.Time `json:"endTime,omitempty"` - Duration *int64 `json:"duration,omitempty"` // milliseconds - LastUpdated *time.Time `json:"lastUpdated,omitempty"` - Commands CommandSummary `json:"commands"` - Attempts int `json:"attempts"` - AttemptHistory []AttemptEntry `json:"attemptHistory,omitempty"` - Error *string `json:"error,omitempty"` + Index int `json:"index"` // Original position + ID string `json:"id"` // Unique flow ID + Name string `json:"name"` // Display name + SourceFile string `json:"sourceFile"` // Path to YAML file + Tags []string `json:"tags,omitempty"` // Tags for filtering + Properties map[string]string `json:"properties,omitempty"` // Custom metadata for junit XML + Device *Device `json:"device,omitempty"` // Device that ran this flow (for multi-device runs) + DataFile string `json:"dataFile"` // Path to flow detail JSON + AssetsDir string `json:"assetsDir"` // Path to assets directory + Status Status `json:"status"` + UpdateSeq uint64 `json:"updateSeq"` + StartTime *time.Time `json:"startTime,omitempty"` + EndTime *time.Time `json:"endTime,omitempty"` + Duration *int64 `json:"duration,omitempty"` // milliseconds + LastUpdated *time.Time `json:"lastUpdated,omitempty"` + Commands CommandSummary `json:"commands"` + Attempts int `json:"attempts"` + AttemptHistory []AttemptEntry `json:"attemptHistory,omitempty"` + Error *string `json:"error,omitempty"` } // CommandSummary contains command counts for a flow. @@ -144,17 +145,18 @@ type AttemptEntry struct { // FlowDetail contains full flow execution details. type FlowDetail struct { - ID string `json:"id"` - Name string `json:"name"` - SourceFile string `json:"sourceFile"` - Tags []string `json:"tags,omitempty"` - Device *Device `json:"device,omitempty"` // Device that ran this flow (for multi-device runs) - StartTime time.Time `json:"startTime"` - EndTime *time.Time `json:"endTime,omitempty"` - Duration *int64 `json:"duration,omitempty"` // milliseconds - Commands []Command `json:"commands"` - Artifacts FlowArtifacts `json:"artifacts"` - ConsoleLogs []ConsoleLog `json:"consoleLogs,omitempty"` // Browser console / page errors captured during the flow (web only) + ID string `json:"id"` + Name string `json:"name"` + SourceFile string `json:"sourceFile"` + Tags []string `json:"tags,omitempty"` + Properties map[string]string `json:"properties,omitempty"` // Custom metadata for junit XML + Device *Device `json:"device,omitempty"` // Device that ran this flow (for multi-device runs) + StartTime time.Time `json:"startTime"` + EndTime *time.Time `json:"endTime,omitempty"` + Duration *int64 `json:"duration,omitempty"` // milliseconds + Commands []Command `json:"commands"` + Artifacts FlowArtifacts `json:"artifacts"` + ConsoleLogs []ConsoleLog `json:"consoleLogs,omitempty"` // Browser console / page errors captured during the flow (web only) } // ConsoleLog represents a single browser console message or uncaught JS