Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
98bc52d
added e2e heatmap and coverage dashboard
rsultana1418 Jun 3, 2026
4df5dc8
removed unused imports
rsultana1418 Jun 3, 2026
f054d0f
added unit tests
rsultana1418 Jun 5, 2026
ec43874
fixed typo errors
rsultana1418 Jun 5, 2026
6a6253a
replaced the dummy playwright report with minimal fields for test
rsultana1418 Jun 5, 2026
37d20f7
Merge pull request #1 from RaihanSultana/e2e-report-ingestion
RaihanSultana Jun 5, 2026
60ce736
fixed merge conflicts
rsultana1418 Jun 5, 2026
550a500
Merge pull request #2 from RaihanSultana/e2e-report-ingestion
RaihanSultana Jun 5, 2026
735937b
replaced logs with slog and added check in e2e migration file
rsultana1418 Jun 8, 2026
cdd3d9b
Merge pull request #3 from RaihanSultana/e2e-report-ingestion
RaihanSultana Jun 8, 2026
fd59222
removed duplicate table drop query
rsultana1418 Jun 8, 2026
fcc2e7b
Merge pull request #4 from RaihanSultana/e2e-report-ingestion
RaihanSultana Jun 8, 2026
66bc156
moved check constraint from the table
rsultana1418 Jun 8, 2026
f620d20
Merge pull request #5 from RaihanSultana/e2e-report-ingestion
RaihanSultana Jun 8, 2026
e23bb0d
added spec type to e2e tests report
rsultana1418 Jun 15, 2026
a40ed33
Merge branch 'main' into e2e-spec-type
rsultana1418 Jun 15, 2026
9263034
added spec type filter in the dashboard and heatmap
RaihanSultana Jun 16, 2026
233b0fb
updated the success ratio
RaihanSultana Jun 16, 2026
9c4268f
refactor to save the spec type
RaihanSultana Jun 16, 2026
4d4e3fb
updated failed spec query
RaihanSultana Jun 16, 2026
8e2a7c5
Merge remote-tracking branch 'upstream'
RaihanSultana Jun 16, 2026
68740ab
Merge branch 'main' into e2e-spec-type
RaihanSultana Jun 16, 2026
c5bf43f
resolved pr comment - refactored spec type normalization and frontend
RaihanSultana Jun 17, 2026
620a3f7
support appium junit report
RaihanSultana Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 221 additions & 18 deletions cmd/coveragecli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"bytes"
"encoding/json"
"encoding/xml"
"errors"
"flag"
"fmt"
Expand All @@ -12,6 +13,7 @@ import (
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"sort"
"strconv"
Expand Down Expand Up @@ -114,6 +116,57 @@ type metricAgg struct {
Total float64
}

// JUnit XML structs — shared between Playwright and Appium JUnit reports.
// JUnitTestSuites represents the root <testsuites> element in JUnit XML.
type JUnitTestSuites struct {
XMLName xml.Name `xml:"testsuites"`
Name string `xml:"name,attr,omitempty"`
Tests int `xml:"tests,attr,omitempty"`
Failures int `xml:"failures,attr,omitempty"`
Errors int `xml:"errors,attr,omitempty"`
Time float64 `xml:"time,attr,omitempty"`
TestSuites []JUnitTestSuite `xml:"testsuite"`
}

// JUnitTestSuite represents a single <testsuite> element in JUnit XML.
type JUnitTestSuite struct {
Name string `xml:"name,attr"`
Tests int `xml:"tests,attr,omitempty"`
Failures int `xml:"failures,attr,omitempty"`
Errors int `xml:"errors,attr,omitempty"`
Skipped int `xml:"skipped,attr,omitempty"`
Time float64 `xml:"time,attr,omitempty"`
Timestamp string `xml:"timestamp,attr,omitempty"`
Hostname string `xml:"hostname,attr,omitempty"`
Properties []JUnitProperty `xml:"properties>property,omitempty"`
TestCases []JUnitTestCase `xml:"testcase"`
SystemOut string `xml:"system-out,omitempty"`
}

type JUnitTestCase struct {
Classname string `xml:"classname,attr,omitempty"`
Name string `xml:"name,attr"`
Time float64 `xml:"time,attr,omitempty"`
Status string `xml:"status,attr,omitempty"`
Failure *JUnitFailure `xml:"failure,omitempty"`
Skipped *JUnitSkipped `xml:"skipped,omitempty"`
}

type JUnitFailure struct {
Message string `xml:"message,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
Body string `xml:",chardata"`
}

type JUnitSkipped struct {
Message string `xml:"message,attr,omitempty"`
}

type JUnitProperty struct {
Name string `xml:"name,attr"`
Value string `xml:"value,attr"`
}

func main() {
if len(os.Args) > 1 {
switch os.Args[1] {
Expand Down Expand Up @@ -437,7 +490,7 @@ func runE2EUpload(args []string) {
triggerType := fs.String("trigger-type", "manual", "Trigger type: push|pr|manual")
environment := fs.String("environment", "", "Environment: test|stage|prod (optional)")
runTimestamp := fs.String("run-timestamp", time.Now().UTC().Format(time.RFC3339), "Run timestamp (RFC3339)")
platformType := fs.String("platform-type", "web", "Platform type: web|android|ios")

if err := fs.Parse(args); err != nil {
exitErr("parse flags", err)
}
Expand All @@ -457,9 +510,16 @@ func runE2EUpload(args []string) {
exitErr("read e2e report", err)
}

var report map[string]any
if err := json.Unmarshal(rawReport, &report); err != nil {
exitErr("parse e2e report json", err)
// Detect file format from extension
ext := strings.ToLower(filepath.Ext(*reportPath))
var isXML bool
switch ext {
case ".xml":
isXML = true
case ".json":
isXML = false
default:
exitErr("validate input", fmt.Errorf("unsupported file extension %q: expected .json or .xml", ext))
}

var group *string
Expand All @@ -475,17 +535,35 @@ func runE2EUpload(args []string) {
env = environment
}

// Normalize report structure based on report type
var normalizeReport map[string]any
switch *reportType {
case "playwright":
normalizeReport = normalizePlaywrightReport(report)
case "appium":
normalizeReport = normalizeAppiumReport(report)
default:
exitErr("validate input", fmt.Errorf("unsupported report type: %s", *reportType))
// Normalize report structure based on report type and file format
var normalizedReport map[string]any
if isXML {
var junitData JUnitTestSuites
if err := xml.Unmarshal(rawReport, &junitData); err != nil {
exitErr("parse e2e report xml", err)
}
switch *reportType {
case "playwright":
normalizedReport = normalizePlaywrightJUnit(junitData)
case "appium":
normalizedReport = normalizeAppiumJUnit(junitData)
default:
exitErr("validate input", fmt.Errorf("unsupported report type: %s", *reportType))
}
} else {
var report map[string]any
if err := json.Unmarshal(rawReport, &report); err != nil {
exitErr("parse e2e report json", err)
}
switch *reportType {
case "playwright":
normalizedReport = normalizePlaywrightReport(report)
case "appium":
exitErr("validate input", fmt.Errorf("appium JSON report format is not yet supported; use JUnit XML (.xml)"))
default:
exitErr("validate input", fmt.Errorf("unsupported report type: %s", *reportType))
}
}
normalizeReport["platformType"] = *platformType

payload := e2ePayload{
ProjectKey: *projectKey,
Expand All @@ -498,7 +576,7 @@ func runE2EUpload(args []string) {
TriggerType: *triggerType,
RunTimestamp: *runTimestamp,
Environment: env,
TestReport: normalizeReport,
TestReport: normalizedReport,
}

body, err := json.MarshalIndent(payload, "", " ")
Expand Down Expand Up @@ -630,6 +708,8 @@ func normalizePlaywrightReport(raw map[string]any) map[string]any {

// Use the last test result (accounts for retries).
tests := firstSlice(specMap, "tests")
file := firstString(suiteMap, "file")
spec_type := "happyPath"
state := "skipped"
runTime := 0.0
var failureBlock map[string]any
Expand Down Expand Up @@ -668,6 +748,24 @@ func normalizePlaywrightReport(raw map[string]any) map[string]any {
}
}
}

// spec_type can be either setup, happyPath or negativePath
// checks if the file name contains "setup", "happyPath" or "negativePath" to determine the spec_type
// fall back to checking the projectId
projectID := firstString(testMap, "projectId")

switch {
case strings.Contains(file, "setup"):
spec_type = "setup"
case strings.Contains(file, "happyPath"):
spec_type = "happyPath"
case strings.Contains(file, "negativePath"):
spec_type = "negativePath"
case projectID == "happypath" || projectID == "negativePath" || projectID == "setup":
spec_type = projectID
default:
spec_type = "happyPath"
}
}
}

Expand All @@ -682,6 +780,8 @@ func normalizePlaywrightReport(raw map[string]any) map[string]any {
"containerHierarchyTexts": hierarchyCopy,
"state": state,
"runTime": runTime,
"suite_type": firstString(suiteMap, "type"),
"specType": spec_type,
}
if failureBlock != nil {
normalized["failure"] = failureBlock
Expand All @@ -695,12 +795,115 @@ func normalizePlaywrightReport(raw map[string]any) map[string]any {
return result
}

func normalizeAppiumReport(raw map[string]any) map[string]any {
// not implemented yet
exitErr("normalize report", fmt.Errorf("appium report normalization not implemented yet"))
// normalizePlaywrightJUnit converts a Playwright JUnit XML report into the normalized map[string]any structure.
// Playwright JUnit uses classname format: "file › Suite Title › Nested Suite"
func normalizePlaywrightJUnit(data JUnitTestSuites) map[string]any {
fmt.Errorf("Playwright JUnit XML normalization is not yet implemented")
return nil
}

// normalizeAppiumJUnit converts an Appium JUnit XML report into the normalized map[string]any structure.
// Appium JUnit uses classname format: "com.package.ClassName" (dot-separated)
func normalizeAppiumJUnit(data JUnitTestSuites) map[string]any {
result := make(map[string]any)
testFramework := "appium"
result["reportType"] = &testFramework
result["testFramework"] = &testFramework

// Use top-level testsuites name as suiteDescription
if data.Name != "" {
result["suiteDescription"] = data.Name
}
result["suitePath"] = data.TestSuites[0].TestCases[0].Classname
result["frameworkVersion"] = ""

// Extract platform metadata from first testsuite's properties
// Set default platform type for Appium
platformType := "android"
if len(data.TestSuites) > 0 {
for _, prop := range data.TestSuites[0].Properties {
switch prop.Name {
case "platformName":
platformType = strings.ToLower(prop.Value)
case "automationName":
result["frameworkVersion"] = prop.Value
}
}
}
result["platformType"] = platformType
fmt.Println("normalize: ", result["platformType"])

var specReports []map[string]any
for _, suite := range data.TestSuites {
for _, tc := range suite.TestCases {
// Appium classname format: "com.package.tests.Login.LoginPass" (split on ".")
var hierarchy []any
if tc.Classname != "" {
parts := strings.Split(tc.Classname, ".")
for _, p := range parts {
if p != "" {
hierarchy = append(hierarchy, p)
}
}
}

// Determine state from failure/skipped elements or status attribute
state := "passed"
if tc.Failure != nil {
state = "failed"
} else if tc.Skipped != nil {
state = "skipped"
} else if tc.Status != "" {
// Some Appium/TestNG reporters include a status attribute
switch strings.ToLower(tc.Status) {
case "passed":
state = "passed"
case "failed":
state = "failed"
case "skipped":
state = "skipped"
}
}

// Determine specType from classname keywords
specType := "happyPath"
classLower := strings.ToLower(tc.Classname)
switch {
case strings.Contains(classLower, "setup"):
specType = "setup"
case strings.Contains(classLower, "happyPath"):
specType = "happyPath"
case strings.Contains(classLower, "negativepath"):
specType = "negativePath"
default:
specType = "happyPath"
}

spec := map[string]any{
"leafNodeText": tc.Name,
"containerHierarchyTexts": hierarchy,
"state": state,
"runTime": tc.Time,
"suite_type": suite.Name,
"specType": specType,
}

if tc.Failure != nil {
failure := map[string]any{
"message": tc.Failure.Message,
}
if tc.Failure.Body != "" {
failure["stackTrace"] = strings.TrimSpace(tc.Failure.Body)
}
spec["failure"] = failure
}
specReports = append(specReports, spec)
}
}
result["specReports"] = specReports
return result
}

// stripANSI removes ANSI escape codes from a string.
// This is useful to clean up error messages from Playwright which may include ANSI codes for coloring.
func stripANSI(s string) string {
Expand Down
Loading
Loading