package shortcode import ( "testing" ) func TestParseAttrs(t *testing.T) { tests := []struct { name string input string expected map[string]string }{ { name: "single unquoted attr", input: `[field idx=5]`, expected: map[string]string{ "idx": "5", }, }, { name: "single quoted attr", input: `[field idx="5"]`, expected: map[string]string{ "idx": "5", }, }, { name: "multiple attrs", input: `[field idx=3 type="text" label="Name"]`, expected: map[string]string{ "idx": "3", "type": "text", "label": "Name", }, }, { name: "tag only no attrs", input: `[field]`, expected: map[string]string{}, }, { name: "empty string returns empty map", input: `[tag]`, expected: map[string]string{}, }, { name: "attr with spaces in quoted value", input: `[field label="First Last"]`, expected: map[string]string{ "label": "First Last", }, }, { name: "mixed quoted and unquoted", input: `[field idx=10 type=text label="My Label"]`, expected: map[string]string{ "idx": "10", "type": "text", "label": "My Label", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := ParseAttrs(tt.input) for k, v := range tt.expected { if got[k] != v { t.Errorf("ParseAttrs(%q)[%q] = %q, want %q", tt.input, k, got[k], v) } } if len(got) != len(tt.expected) { t.Errorf("ParseAttrs(%q) returned %d keys, want %d: %v", tt.input, len(got), len(tt.expected), got) } }) } } func TestShortcodesRe(t *testing.T) { tests := []struct { name string input string wantLen int }{ { name: "single shortcode", input: `[field idx=1]`, wantLen: 1, }, { name: "multiple shortcodes", input: `[field idx=1][field idx=2]`, wantLen: 2, }, { name: "shortcode in text", input: `Hello [field idx=1] world`, wantLen: 1, }, { name: "no shortcodes", input: `Just plain text`, wantLen: 0, }, { name: "empty string", input: ``, wantLen: 0, }, { name: "brackets but not shortcode", input: `(not a shortcode)`, wantLen: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := ShortcodesRe.FindAllString(tt.input, -1) if len(got) != tt.wantLen { t.Errorf("ShortcodesRe.FindAllString(%q) = %d matches, want %d: %v", tt.input, len(got), tt.wantLen, got) } }) } } func TestLegacyMarkerRe(t *testing.T) { tests := []struct { name string input string wantLen int }{ { name: "single legacy marker", input: `[F0]`, wantLen: 1, }, { name: "multiple legacy markers", input: `[F0] [F12] [F99]`, wantLen: 3, }, { name: "no legacy markers", input: `Hello world`, wantLen: 0, }, { name: "empty string", input: ``, wantLen: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := LegacyMarkerRe.FindAllString(tt.input, -1) if len(got) != tt.wantLen { t.Errorf("LegacyMarkerRe.FindAllString(%q) = %d, want %d: %v", tt.input, len(got), tt.wantLen, got) } }) } } func TestHasShortcodes(t *testing.T) { tests := []struct { name string input string want bool }{ {name: "has shortcode", input: `[field idx=1]`, want: true}, {name: "has multiple", input: `[field idx=1][button]`, want: true}, {name: "plain text", input: `Hello`, want: false}, {name: "empty", input: ``, want: false}, {name: "brackets only", input: `[]`, want: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := HasShortcodes(tt.input); got != tt.want { t.Errorf("HasShortcodes(%q) = %v, want %v", tt.input, got, tt.want) } }) } } func TestHasFields(t *testing.T) { tests := []struct { name string input string want bool }{ {name: "field shortcode", input: `[field idx=1]`, want: true}, {name: "legacy marker", input: `[F5]`, want: true}, {name: "both", input: `[field idx=1][F0]`, want: true}, {name: "no fields", input: `plain text`, want: false}, {name: "empty", input: ``, want: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := HasFields(tt.input); got != tt.want { t.Errorf("HasFields(%q) = %v, want %v", tt.input, got, tt.want) } }) } } func TestParseFields(t *testing.T) { tests := []struct { name string input string wantLen int wantIdx []int }{ { name: "single field", input: `[field idx=3]`, wantLen: 1, wantIdx: []int{3}, }, { name: "multiple fields", input: `[field idx=1 type="text"][field idx=2 type="email"]`, wantLen: 2, wantIdx: []int{1, 2}, }, { name: "no fields", input: `plain text`, wantLen: 0, }, { name: "empty string", input: ``, wantLen: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := GetFields(tt.input) if len(got) != tt.wantLen { t.Fatalf("GetFields(%q) returned %d fields, want %d", tt.input, len(got), tt.wantLen) } for i, f := range got { if f.Idx != tt.wantIdx[i] { t.Errorf("GetFields(%q)[%d].Idx = %d, want %d", tt.input, i, f.Idx, tt.wantIdx[i]) } } }) } } func TestParseFieldsMissingIdx(t *testing.T) { // Fields without idx should be skipped input := `[field type="text" label="No Idx"]` got := GetFields(input) if len(got) != 0 { t.Errorf("GetFields(%q) returned %d fields, want 0 (missing idx should skip)", input, len(got)) } } func TestParseFieldsInvalidIdx(t *testing.T) { // Fields with non-numeric idx should be skipped input := `[field idx=abc]` got := GetFields(input) if len(got) != 0 { t.Errorf("GetFields(%q) returned %d fields, want 0 (invalid idx should skip)", input, len(got)) } } func TestParseLegacyMarkers(t *testing.T) { tests := []struct { name string input string wantLen int wantIdx []int }{ { name: "single legacy marker", input: `[F0]`, wantLen: 1, wantIdx: []int{0}, }, { name: "multiple legacy markers", input: `[F5] text [F12] more [F3]`, wantLen: 3, wantIdx: []int{5, 12, 3}, }, { name: "no legacy markers", input: `plain text`, wantLen: 0, }, { name: "empty string", input: ``, wantLen: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := GetFields(tt.input) if len(got) != tt.wantLen { t.Fatalf("GetFields(%q) returned %d fields, want %d", tt.input, len(got), tt.wantLen) } for i, f := range got { if f.Idx != tt.wantIdx[i] { t.Errorf("GetFields(%q)[%d].Idx = %d, want %d", tt.input, i, f.Idx, tt.wantIdx[i]) } // Legacy markers get default type "text" from GetFields if f.Type != "text" { t.Errorf("GetFields(%q)[%d].Type = %q, want 'text' (default for legacy marker)", tt.input, i, f.Type) } } }) } } func TestGetFieldsWithDefaults(t *testing.T) { tests := []struct { name string input string defaultTypes map[int]string wantType []string }{ { name: "type from shortcode", input: `[field idx=1 type="email"]`, defaultTypes: nil, wantType: []string{"email"}, }, { name: "type from defaults", input: `[field idx=2]`, defaultTypes: map[int]string{2: "tel"}, wantType: []string{"tel"}, }, { name: "type from defaults falls back to text", input: `[field idx=3]`, defaultTypes: map[int]string{}, wantType: []string{"text"}, }, { name: "shortcode type overrides default", input: `[field idx=4 type="url"]`, defaultTypes: map[int]string{4: "date"}, wantType: []string{"url"}, }, { name: "legacy marker uses default", input: `[F5]`, defaultTypes: map[int]string{5: "date"}, wantType: []string{"date"}, }, { name: "legacy marker falls back to text", input: `[F6]`, defaultTypes: nil, wantType: []string{"text"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := GetFieldsWithDefaults(tt.input, tt.defaultTypes) if len(got) != len(tt.wantType) { t.Fatalf("GetFieldsWithDefaults(%q) returned %d fields, want %d", tt.input, len(got), len(tt.wantType)) } for i, f := range got { if f.Type != tt.wantType[i] { t.Errorf("GetFieldsWithDefaults(%q)[%d].Type = %q, want %q", tt.input, i, f.Type, tt.wantType[i]) } } }) } } func TestParseMixedShortcodesAndLegacy(t *testing.T) { input := `[field idx=1 type="text" label="Name"] and [F2] and [field idx=3]` fields := GetFieldsWithDefaults(input, map[int]string{2: "email"}) if len(fields) != 3 { t.Fatalf("expected 3 fields, got %d", len(fields)) } // Build a map by idx since map iteration order is not guaranteed fieldMap := make(map[int]*Field) for _, f := range fields { fieldMap[f.Idx] = f } if f, ok := fieldMap[1]; !ok || f.Type != "text" { t.Errorf("field idx=1: got %v, want Type=text", f) } if f, ok := fieldMap[2]; !ok || f.Type != "email" { t.Errorf("field idx=2: got %v, want Type=email", f) } if f, ok := fieldMap[3]; !ok || f.Type != "text" { t.Errorf("field idx=3: got %v, want Type=text", f) } } func TestFieldHandlerParse(t *testing.T) { h := FieldHandler{} t.Run("tag is field", func(t *testing.T) { if h.Tag() != "field" { t.Errorf("FieldHandler.Tag() = %q, want 'field'", h.Tag()) } }) t.Run("valid attrs", func(t *testing.T) { attrs := map[string]string{ "idx": "42", "type": "email", "label": "Test", "group": "grp", } parsed, _ := h.Parse(attrs) f, ok := parsed.(*Field) if !ok { t.Fatalf("Parse returned %T, want *Field", parsed) } if f.Idx != 42 { t.Errorf("Idx = %d, want 42", f.Idx) } if f.Type != "email" { t.Errorf("Type = %q, want 'email'", f.Type) } if f.Label != "Test" { t.Errorf("Label = %q, want 'Test'", f.Label) } if f.Group != "grp" { t.Errorf("Group = %q, want 'grp'", f.Group) } }) t.Run("missing idx returns nil", func(t *testing.T) { attrs := map[string]string{ "type": "text", "label": "No Index", } parsed, _ := h.Parse(attrs) if parsed != nil { t.Errorf("Parse with missing idx returned %v, want nil", parsed) } }) t.Run("non-numeric idx returns nil", func(t *testing.T) { attrs := map[string]string{ "idx": "notanumber", } parsed, _ := h.Parse(attrs) if parsed != nil { t.Errorf("Parse with non-numeric idx returned %v, want nil", parsed) } }) t.Run("empty idx string returns nil", func(t *testing.T) { attrs := map[string]string{ "idx": "", } parsed, _ := h.Parse(attrs) if parsed != nil { t.Errorf("Parse with empty idx returned %v, want nil", parsed) } }) } func TestParseMultipleShortcodeTypes(t *testing.T) { result := Parse(`[field idx=1][field idx=2]`) fieldResults, ok := result["field"] if !ok { t.Fatal("Parse did not return 'field' key") } if len(fieldResults) != 2 { t.Errorf("Parse returned %d field results, want 2", len(fieldResults)) } }