Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,45 @@ import qascsv "github.com/hypersequent/qasphere-csv"

Refer to the [basic example](examples/basic/main.go) for API usage.

## Steps and Test Data

`Step.Action` and `Step.Expected` remain the API for ordinary test steps. The library serializes all steps into QA Sphere's modern JSON `Steps` CSV column:

```go
Steps: []qascsv.Step{{
Action: "Run the query",
Expected: "One row is returned",
Data: []qascsv.StepData{
qascsv.StepDataText{Label: "Query", Value: "SELECT 1", Format: "sql"},
qascsv.StepDataLink{Label: "Reference", Value: "https://example.com/spec"},
qascsv.StepDataFile{Label: "Fixture", Value: qascsv.File{
Name: "fixture.csv", ID: "uploaded-file-id", URL: "https://example.com/fixture.csv",
MimeType: "text/csv", Size: 128,
}},
},
}}
```

Shared steps can reference an existing QA Sphere shared step by ID, or include a title and sub-steps so the shared step can be recreated during import. Test data belongs on sub-steps, not directly on the shared row:

```go
Steps: []qascsv.Step{{
SharedStepID: 42,
}, {
Title: "Sign in",
SubSteps: []qascsv.Step{{
Action: "Enter the credentials",
Data: []qascsv.StepData{
qascsv.StepDataText{Label: "User", Value: "alice"},
},
}},
}}
```

Each standalone step or shared-step sub-step accepts at most 20 data items. Labels and shared-step titles are limited to 255 Unicode characters. Text values are required and limited to 65,535 UTF-16 code units; their optional formats are limited to 32 characters. Link values must be HTTP(S) URLs no longer than 255 characters. File data uses the same validation and JSON shape as test-case files.

> **Migration note:** generated CSVs now always contain one JSON `Steps` column after `Preconditions`. Earlier releases generated variable `Step N` and `Expected N` columns; consumers that inspect headers or post-process CSV output must update to the modern column.

## Importing Test Cases on QA Sphere

1. Create a new Project, if not already done.
Expand Down
13 changes: 13 additions & 0 deletions examples/basic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ func main() {
Preconditions: "The \"About Us\" page is opened",
Steps: []qascsv.Step{{
Action: "Test the display across various screen sizes (desktop, tablet, mobile) to ensure that blocks and buttons adjust appropriately to different viewport widths",
Data: []qascsv.StepData{
qascsv.StepDataText{Label: "Viewports", Value: "1440x900, 768x1024, 390x844"},
qascsv.StepDataLink{Label: "Design", Value: "https://example.com/about-us-design"},
},
}},
}); err != nil {
log.Fatal("failed to add single test case", err)
Expand All @@ -38,6 +42,15 @@ func main() {
}, {
Action: "Click the \"Cart\" icon",
Expected: "The empty state is shown in the \"Cart\" modal",
}, {
Title: "Verify an empty cart",
SubSteps: []qascsv.Step{{
Action: "Inspect the cart item count",
Expected: "The item count is zero",
Data: []qascsv.StepData{
qascsv.StepDataText{Label: "Expected count", Value: "0"},
},
}},
}},
}, {
Title: "Changing to corresponding cursor after hovering the element",
Expand Down
191 changes: 169 additions & 22 deletions qacsv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ var successTestCases = []TestCase{
},
}

const successTestCasesCSV = `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params,Step 1,Expected 1,Step 2,Expected 2
root/child,,standalone,tc-with-all-fields,legacy-id,false,high,"tag1,tag2",[req1](http://req1),"[link-1](http://link1),[link-2](http://link2)","[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10},{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]",preconditions,,,action-1,expected-1,action-2,expected-2
root/child,,standalone,"tc-with-special-chars.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",legacy-id,false,high,"tag1.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;","[req.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;]()","[link-1.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;](http://link1)","[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]","preconditions.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",,,"action.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;","expected.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",,
root,,standalone,tc-with-minimal-fields,,false,high,,,,,,,,,,,
root,,standalone,tc-with-partial-fields,,true,low,,[](http://req1),,"[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10},{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]",,,,action-1,,,expected-2
const successTestCasesCSV = `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Steps,Parameter Values,Template Suffix Params
root/child,,standalone,tc-with-all-fields,legacy-id,false,high,"tag1,tag2",[req1](http://req1),"[link-1](http://link1),[link-2](http://link2)","[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10},{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]",preconditions,"[{""description"":""action-1"",""expected"":""expected-1""},{""description"":""action-2"",""expected"":""expected-2""}]",,
root/child,,standalone,"tc-with-special-chars.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",legacy-id,false,high,"tag1.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;","[req.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;]()","[link-1.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;](http://link1)","[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]","preconditions.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;","[{""description"":""action.,<>/@$%\""\""''*&()[]{}+-[BACKTICK]!~;"",""expected"":""expected.,<>/@$%\""\""''*&()[]{}+-[BACKTICK]!~;""}]",,
root,,standalone,tc-with-minimal-fields,,false,high,,,,,,,,
root,,standalone,tc-with-partial-fields,,true,low,,[](http://req1),,"[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10},{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]",,"[{""description"":""action-1""},{""expected"":""expected-2""}]",,
`

var failureTestCases = []TestCase{
Expand Down Expand Up @@ -355,12 +355,12 @@ var customFieldSuccessTestCases = []TestCase{
},
}

const customFieldSuccessTestCasesCSV = `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params,Step 1,Expected 1,custom_field_dropdown_test_env,custom_field_dropdown_automation,custom_field_text_notes
custom-fields,,standalone,tc-with-single-custom-field,,false,medium,,,,,,,,,,"{""value"":""staging"",""isDefault"":false}",,
custom-fields,,standalone,tc-with-multiple-custom-fields,,false,high,"regression,smoke",,,,,,,Execute test,Test passes,"{""value"":""production"",""isDefault"":false}","{""value"":""Automated"",""isDefault"":false}","{""value"":""This is a test note with special chars: !@#$%^&*()"",""isDefault"":false}"
custom-fields,,standalone,tc-with-empty-custom-field-value,,false,low,,,,,,,,,,,,"{""value"":"""",""isDefault"":false}"
custom-fields,,standalone,tc-with-default-custom-field,,false,medium,,,,,,,,,,,"{""value"":"""",""isDefault"":false}",
custom-fields/comprehensive,,standalone,tc-with-all-fields-and-custom-fields,CF-001,false,high,"custom,comprehensive",[CF Requirements](http://cf-req),[CF Link](http://cf-link),"[{""fileName"":""cf-test.txt"",""id"":""cf-file-id"",""url"":""http://cf-file"",""mimeType"":""text/plain"",""size"":100}]",Custom field test setup,,,Step 1,Result 1,"{""value"":""development"",""isDefault"":false}","{""value"":""In Progress"",""isDefault"":false}",
const customFieldSuccessTestCasesCSV = `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Steps,Parameter Values,Template Suffix Params,custom_field_dropdown_test_env,custom_field_dropdown_automation,custom_field_text_notes
custom-fields,,standalone,tc-with-single-custom-field,,false,medium,,,,,,,,,"{""value"":""staging"",""isDefault"":false}",,
custom-fields,,standalone,tc-with-multiple-custom-fields,,false,high,"regression,smoke",,,,,"[{""description"":""Execute test"",""expected"":""Test passes""}]",,,"{""value"":""production"",""isDefault"":false}","{""value"":""Automated"",""isDefault"":false}","{""value"":""This is a test note with special chars: !@#$%^&*()"",""isDefault"":false}"
custom-fields,,standalone,tc-with-empty-custom-field-value,,false,low,,,,,,,,,,,"{""value"":"""",""isDefault"":false}"
custom-fields,,standalone,tc-with-default-custom-field,,false,medium,,,,,,,,,,"{""value"":"""",""isDefault"":false}",
custom-fields/comprehensive,,standalone,tc-with-all-fields-and-custom-fields,CF-001,false,high,"custom,comprehensive",[CF Requirements](http://cf-req),[CF Link](http://cf-link),"[{""fileName"":""cf-test.txt"",""id"":""cf-file-id"",""url"":""http://cf-file"",""mimeType"":""text/plain"",""size"":100}]",Custom field test setup,"[{""description"":""Step 1"",""expected"":""Result 1""}]",,,"{""value"":""development"",""isDefault"":false}","{""value"":""In Progress"",""isDefault"":false}",
`

var customFieldFailureTestCases = []TestCase{
Expand Down Expand Up @@ -530,8 +530,8 @@ func TestFolderSlashEscaping(t *testing.T) {
csv, err := qasCSV.GenerateCSV()
require.NoError(t, err)

expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params
root\/parent/child\/leaf,,standalone,tc-in-slash-folder,,false,high,,,,,,,
expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Steps,Parameter Values,Template Suffix Params
root\/parent/child\/leaf,,standalone,tc-in-slash-folder,,false,high,,,,,,,,
`
require.Equal(t, expected, csv)
}
Expand All @@ -557,8 +557,8 @@ func TestAddFolderEmpty(t *testing.T) {
csv, err := qasCSV.GenerateCSV()
require.NoError(t, err)

expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params
empty-folder,,,,,,,,,,,,,
expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Steps,Parameter Values,Template Suffix Params
empty-folder,,,,,,,,,,,,,,
`
require.Equal(t, expected, csv)
}
Expand All @@ -575,8 +575,8 @@ func TestAddFolderWithComment(t *testing.T) {
csv, err := qasCSV.GenerateCSV()
require.NoError(t, err)

expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params
commented-folder,This is a folder comment,,,,,,,,,,,,
expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Steps,Parameter Values,Template Suffix Params
commented-folder,This is a folder comment,,,,,,,,,,,,,
`
require.Equal(t, expected, csv)
}
Expand All @@ -600,9 +600,9 @@ func TestAddFolderWithCommentAndTestCases(t *testing.T) {
csv, err := qasCSV.GenerateCSV()
require.NoError(t, err)

expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params
my-folder,Folder description,,,,,,,,,,,,
my-folder,,standalone,tc-in-commented-folder,,false,high,,,,,,,
expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Steps,Parameter Values,Template Suffix Params
my-folder,Folder description,,,,,,,,,,,,,
my-folder,,standalone,tc-in-commented-folder,,false,high,,,,,,,,
`
require.Equal(t, expected, csv)
}
Expand Down Expand Up @@ -662,8 +662,155 @@ func TestAddFolderWithSlashInName(t *testing.T) {
csv, err := qasCSV.GenerateCSV()
require.NoError(t, err)

expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params
folder\/with\/slashes/child,slash comment,,,,,,,,,,,,
expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Steps,Parameter Values,Template Suffix Params
folder\/with\/slashes/child,slash comment,,,,,,,,,,,,,
`
require.Equal(t, expected, csv)
}

func TestModernStepsSerialization(t *testing.T) {
file := File{
Name: "evidence.txt",
ID: "file-id",
URL: "https://files.example.com/evidence.txt",
MimeType: "text/plain",
Size: 12,
}
qasCSV := NewQASphereCSV()
require.NoError(t, qasCSV.AddTestCase(TestCase{
Title: "modern steps",
FolderPath: []string{"root"},
Priority: PriorityHigh,
Steps: []Step{
{},
{Action: "Open the page", Expected: "The page opens"},
{Data: []StepData{
StepDataText{Label: "Input", Value: "SELECT 1", Format: "sql"},
StepDataLink{Value: "https://example.com"},
StepDataFile{Label: "Evidence", Value: file},
}},
{SharedStepID: 42},
{
Title: "Sign in",
SubSteps: []Step{
{},
{Action: "Enter credentials", Data: []StepData{StepDataText{Value: "alice"}}},
},
},
},
}))

csvText, err := qasCSV.GenerateCSV()
require.NoError(t, err)
records, err := csv.NewReader(strings.NewReader(csvText)).ReadAll()
require.NoError(t, err)
require.Len(t, records, 2)
require.Equal(t, "Steps", records[0][12])
require.Equal(t, "Parameter Values", records[0][13])
require.JSONEq(t, `[
{"description":"Open the page","expected":"The page opens"},
{"data":[
{"type":"text","label":"Input","data":{"value":"SELECT 1","format":"sql"}},
{"type":"link","data":{"value":"https://example.com"}},
{"type":"file","label":"Evidence","data":{"value":{"fileName":"evidence.txt","id":"file-id","url":"https://files.example.com/evidence.txt","mimeType":"text/plain","size":12}}}
]},
{"sharedStepId":42},
{"title":"Sign in","subSteps":[{"description":"Enter credentials","data":[{"type":"text","data":{"value":"alice"}}]}]}
]`, records[1][12])
}

func TestEmptyStepsCell(t *testing.T) {
qasCSV := NewQASphereCSV()
require.NoError(t, qasCSV.AddTestCase(testCaseWithSteps([]Step{{}, {Action: "", Expected: ""}})))

csvText, err := qasCSV.GenerateCSV()
require.NoError(t, err)
records, err := csv.NewReader(strings.NewReader(csvText)).ReadAll()
require.NoError(t, err)
require.Empty(t, records[1][12])
}

func TestStepDataValidationBoundaries(t *testing.T) {
validFile := File{Name: "file", ID: "id", URL: "https://example.com/file", MimeType: "text/plain", Size: 1}
validURL := "https://example.com/" + strings.Repeat("a", 255-len("https://example.com/"))
validCases := map[string][]Step{
"20 data items": {{Data: repeatStepData(20)}},
"20 sub-step data items": {{Title: "shared", SubSteps: []Step{{Data: repeatStepData(20)}}}},
"255 character label": {{Data: []StepData{StepDataText{Label: strings.Repeat("界", 255), Value: "v"}}}},
"65535 UTF-16 code units": {{Data: []StepData{StepDataText{Value: strings.Repeat("😀", 32767) + "a"}}}},
"32 character format": {{Data: []StepData{StepDataText{Value: "v", Format: strings.Repeat("界", 32)}}}},
"255 character link": {{Data: []StepData{StepDataLink{Value: validURL}}}},
"valid file": {{Data: []StepData{StepDataFile{Value: validFile}}}},
}
for name, steps := range validCases {
t.Run(name, func(t *testing.T) {
require.NoError(t, NewQASphereCSV().AddTestCase(testCaseWithSteps(steps)))
})
}
}

func TestStepValidationFailures(t *testing.T) {
var nilText *StepDataText
invalidCases := map[string][]Step{
"too many data items": {{Data: repeatStepData(21)}},
"too many sub-step data items": {{Title: "shared", SubSteps: []Step{{Data: repeatStepData(21)}}}},
"long label": {{Data: []StepData{StepDataText{Label: strings.Repeat("界", 256), Value: "v"}}}},
"missing text": {{Data: []StepData{StepDataText{}}}},
"long UTF-16 text": {{Data: []StepData{StepDataText{Value: strings.Repeat("😀", 32768)}}}},
"long format": {{Data: []StepData{StepDataText{Value: "v", Format: strings.Repeat("界", 33)}}}},
"missing link": {{Data: []StepData{StepDataLink{}}}},
"non-HTTP link": {{Data: []StepData{StepDataLink{Value: "ftp://example.com/file"}}}},
"long link": {{Data: []StepData{StepDataLink{Value: "https://example.com/" + strings.Repeat("a", 256)}}}},
"invalid file": {{Data: []StepData{StepDataFile{Value: File{}}}}},
"nil data": {{Data: []StepData{nil}}},
"typed nil data": {{Data: []StepData{nilText}}},
"negative shared ID": {{SharedStepID: -1}},
"long shared title": {{Title: strings.Repeat("界", 256)}},
"shared action": {{Title: "shared", Action: "not allowed"}},
"shared expected": {{SharedStepID: 1, Expected: "not allowed"}},
"shared data": {{Title: "shared", Data: []StepData{StepDataText{Value: "v"}}}},
"sub-steps without shared metadata": {{SubSteps: []Step{{Action: "child"}}}},
"sub-steps on standalone content": {{Action: "action", SubSteps: []Step{{Action: "child"}}}},
"nested shared ID": {{Title: "shared", SubSteps: []Step{{SharedStepID: 1}}}},
"nested shared title": {{Title: "shared", SubSteps: []Step{{Title: "nested"}}}},
"nested sub-steps": {{Title: "shared", SubSteps: []Step{{SubSteps: []Step{{Action: "nested"}}}}}},
}
for name, steps := range invalidCases {
t.Run(name, func(t *testing.T) {
require.Error(t, NewQASphereCSV().AddTestCase(testCaseWithSteps(steps)))
})
}
}

func TestAddTestCasesStepValidationIsAtomicAndIndexed(t *testing.T) {
qasCSV := NewQASphereCSV()
testCases := []TestCase{
testCaseWithSteps([]Step{{Action: "valid"}}),
testCaseWithSteps([]Step{{Data: repeatStepData(21)}}),
}
testCases[0].Title = "valid"
testCases[1].Title = "invalid"

err := qasCSV.AddTestCases(testCases)
require.Error(t, err)
require.Contains(t, err.Error(), "test case 1")
require.Zero(t, qasCSV.numTCases)
require.Empty(t, qasCSV.folderOrder)
}

func testCaseWithSteps(steps []Step) TestCase {
return TestCase{
Title: "steps test",
FolderPath: []string{"root"},
Priority: PriorityHigh,
Steps: steps,
}
}

func repeatStepData(count int) []StepData {
items := make([]StepData, count)
for i := range items {
items[i] = StepDataText{Value: "value"}
}
return items
}
Loading
Loading