diff --git a/README.md b/README.md index 8bb9642..8034037 100644 --- a/README.md +++ b/README.md @@ -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. `
…
`, `…`), 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: "Verifies the standard login flow.
"}, + }, +}) +``` + +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. diff --git a/qacsv_test.go b/qacsv_test.go index 35fab5a..62d7844 100644 --- a/qacsv_test.go +++ b/qacsv_test.go @@ -1,6 +1,8 @@ package qascsv import ( + "encoding/csv" + "encoding/json" "io" "os" "strings" @@ -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 }, }, }, @@ -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 := "This is a \"long\" description, with commas.
\n" + + "func main() {\n\tfmt.Println(\"hello\")\n}\n" +
+ "" + strings.Repeat("Lorem ipsum dolor sit amet. ", 20) + "
" + 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() diff --git a/qascsv.go b/qascsv.go index a4f5166..4b53781 100644 --- a/qascsv.go +++ b/qascsv.go @@ -11,6 +11,7 @@ import ( "os" "strconv" "strings" + "unicode/utf8" "github.com/go-playground/validator/v10" "github.com/hashicorp/go-multierror" @@ -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. "…
" or "…".
+ // 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"`
}
@@ -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)
@@ -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)
+ }
}
}