Skip to content
Merged
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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,34 @@ Refer to the [basic example](examples/basic/main.go) for API usage.

For more details, please check the [documentation](https://docs.qasphere.com/).

## Custom Fields

Custom fields must be declared with `AddCustomField`/`AddCustomFields` before adding test cases that use them. Three types are supported:

- `text` — plain text, no length limit.
- `dropdown` — the value must match one of the options defined for the field in QA Sphere (option values are limited to 255 characters).
- `richtext` — rich text, no length limit. **Values are HTML** (e.g. `<p>…</p>`, `<pre><code>…</code></pre>`), unlike `Preconditions` and `Steps`, which take markdown. QA Sphere sanitizes the HTML on import using an allowlist of tags and attributes.

For example, to populate QA Sphere's rich text Description field:

```go
qasCSV := qascsv.NewQASphereCSV()
_ = qasCSV.AddCustomField(qascsv.CustomField{
SystemName: "description",
Type: qascsv.CustomFieldTypeRichtext,
})
_ = qasCSV.AddTestCase(qascsv.TestCase{
Title: "Login with valid credentials",
FolderPath: []string{"Auth"},
Priority: qascsv.PriorityHigh,
CustomFields: map[string]qascsv.CustomFieldValue{
"description": {Value: "<p>Verifies the standard login flow.</p>"},
},
})
```

This produces a `custom_field_richtext_description` column matching QA Sphere's CSV export format.

## Contributing

We welcome contributions! If you have a feature request, encounter a problem, or have questions, please [create a new issue](https://github.com/Hypersequent/qasphere-csv/issues/new/choose). You can also contribute by opening a pull request.
Expand Down
66 changes: 63 additions & 3 deletions qacsv_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package qascsv

import (
"encoding/csv"
"encoding/json"
"io"
"os"
"strings"
Expand Down Expand Up @@ -373,12 +375,12 @@ var customFieldFailureTestCases = []TestCase{
},
},
{
Title: "tc-with-very-long-custom-field-value",
Title: "tc-with-very-long-dropdown-value",
FolderPath: []string{"custom-fields-errors"},
Priority: "medium",
CustomFields: map[string]CustomFieldValue{
"notes": {
Value: strings.Repeat("a", 256), // Exceeds 255 char limit
"test_env": {
Value: strings.Repeat("a", 256), // Dropdown options are limited to 255 chars
},
},
},
Expand Down Expand Up @@ -457,6 +459,64 @@ func TestCustomFieldFailureTestCases(t *testing.T) {
}
}

func TestRichtextCustomField(t *testing.T) {
qasCSV := NewQASphereCSV()
require.NoError(t, qasCSV.AddCustomField(CustomField{
SystemName: "description",
Type: CustomFieldTypeRichtext,
}))

// Long multi-line HTML value, well over 255 chars, with quotes and commas
// to exercise CSV and JSON escaping
longHTML := "<p>This is a \"long\" description, with commas.</p>\n" +
"<pre><code>func main() {\n\tfmt.Println(\"hello\")\n}</code></pre>\n" +
"<p>" + strings.Repeat("Lorem ipsum dolor sit amet. ", 20) + "</p>"
require.Greater(t, len(longHTML), 255)

require.NoError(t, qasCSV.AddTestCase(TestCase{
Title: "tc-with-richtext-description",
FolderPath: []string{"richtext"},
Priority: "medium",
CustomFields: map[string]CustomFieldValue{
"description": {Value: longHTML},
},
}))

csvStr, err := qasCSV.GenerateCSV()
require.NoError(t, err)

// Parse the CSV back and verify the value round-trips
records, err := csv.NewReader(strings.NewReader(csvStr)).ReadAll()
require.NoError(t, err)
require.Len(t, records, 2)

header := records[0]
require.Equal(t, "custom_field_richtext_description", header[len(header)-1])

var cfValue CustomFieldValue
require.NoError(t, json.Unmarshal([]byte(records[1][len(header)-1]), &cfValue))
require.Equal(t, longHTML, cfValue.Value)
}

func TestLongTextCustomFieldValue(t *testing.T) {
qasCSV := NewQASphereCSV()
require.NoError(t, qasCSV.AddCustomField(CustomField{
SystemName: "notes",
Type: CustomFieldTypeText,
}))

// Text custom field values have no length limit
err := qasCSV.AddTestCase(TestCase{
Title: "tc-with-long-text-value",
FolderPath: []string{"root"},
Priority: "low",
CustomFields: map[string]CustomFieldValue{
"notes": {Value: strings.Repeat("a", 600)},
},
})
require.NoError(t, err)
}

func TestFolderSlashEscaping(t *testing.T) {
qasCSV := NewQASphereCSV()

Expand Down
41 changes: 31 additions & 10 deletions qascsv.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os"
"strconv"
"strings"
"unicode/utf8"

"github.com/go-playground/validator/v10"
"github.com/hashicorp/go-multierror"
Expand Down Expand Up @@ -87,17 +88,31 @@ type ParameterValue struct {
type CustomFieldType string

const (
CustomFieldTypeText CustomFieldType = "text"
// CustomFieldTypeText is a plain text field.
CustomFieldTypeText CustomFieldType = "text"
// CustomFieldTypeDropdown is a selection field. The value must match one
// of the options defined for the field in QA Sphere (option values are
// limited to 255 characters).
CustomFieldTypeDropdown CustomFieldType = "dropdown"
// CustomFieldTypeRichtext is a rich text field (e.g. the Description
// field). Unlike Preconditions and Steps, which take markdown, richtext
// values are HTML, e.g. "<p>…</p>" or "<pre><code>…</code></pre>".
// QA Sphere sanitizes the HTML on import using an allowlist of tags and
// attributes; disallowed markup is stripped.
CustomFieldTypeRichtext CustomFieldType = "richtext"
)

type CustomField struct {
SystemName string `validate:"required,max=64"`
Type CustomFieldType `validate:"required,oneof=text dropdown"`
Type CustomFieldType `validate:"required,oneof=text dropdown richtext"`
}

// CustomFieldValue represents the value of a custom field on a test case.
// QA Sphere does not limit the length of custom field values, but dropdown
// values must match one of the field's options, which are limited to 255
// characters.
type CustomFieldValue struct {
Value string `json:"value" validate:"max=255"`
Value string `json:"value"`
IsDefault bool `json:"isDefault" validate:"omitempty"`
}

Expand Down Expand Up @@ -128,8 +143,9 @@ type TestCase struct {
// filter or organise related test cases and also helps in creating
// test runs. (optional)
Tags []string `validate:"dive,required,max=255"`
// The preconditions (or description) for the test case. Markdown is
// supported. (optional)
// The preconditions for the test case. Markdown is supported. (optional)
// For test case descriptions, use a richtext custom field instead —
// see CustomFieldTypeRichtext.
Preconditions string
// The sequence of (ordered) actions to be performed while executing
// the test case. (optional)
Expand Down Expand Up @@ -311,17 +327,22 @@ func (q *QASphereCSV) validateTestCase(tc TestCase) error {
}

if tc.CustomFields != nil {
for systemName := range tc.CustomFields {
var found bool
for _, cf := range q.customFields {
for systemName, cfValue := range tc.CustomFields {
var found *CustomField
for i, cf := range q.customFields {
if cf.SystemName == systemName {
found = true
found = &q.customFields[i]
break
}
}
if !found {
if found == nil {
return errors.Errorf("custom field %s is not defined in QASphereCSV.customFields", systemName)
}
// Dropdown values must match an option defined in QA Sphere,
// and options are limited to 255 characters.
if found.Type == CustomFieldTypeDropdown && utf8.RuneCountInString(cfValue.Value) > 255 {
return errors.Errorf("custom field %s: dropdown value must not exceed 255 characters", systemName)
}
}
}

Expand Down
Loading