package main import ( "os" "regexp" "strings" "testing" "hamlog/internal/qso" ) // Every field the bulk-edit dialog OFFERS must be one the repository accepts. // // These two lists live in different packages — a TypeScript field table on one // side, a Go whitelist on the other — and they drifted apart the day the // confirmation dates were added: the new columns went into the QSL Manager's // filter whitelist instead of the bulk-edit one, so the dialog listed fields it // could not write and the operator got "field … is not bulk-editable" only after // selecting 54 QSOs and clicking Apply. Nothing in the build caught it. // // The test reads the ACTUAL field ids out of the .tsx rather than a copy, so a // field added to the UI alone fails here immediately. func TestBulkEditFieldsAreWritable(t *testing.T) { src, err := os.ReadFile("frontend/src/components/BulkEditModal.tsx") if err != nil { t.Skipf("frontend source not available: %v", err) } ids := regexp.MustCompile(`\{ id: '([a-z0-9_]+)'`).FindAllStringSubmatch(string(src), -1) if len(ids) < 10 { t.Fatalf("only %d field ids found — the parse is wrong, not the data", len(ids)) } for _, m := range ids { id := m[1] // Handled before the column map: freq takes a numeric path (freq_hz + // band together) and the extras live in extras_json. if id == "freq" || qso.IsBulkEditableExtra(id) { continue } col, ok := bulkFieldColumns[id] if !ok { t.Errorf("bulk-edit UI offers %q, which maps to no column (bulkFieldColumns)", id) continue } // Extras live outside the qso table (owner_callsign and friends): they are // written into extras_json, not a column, so the column whitelist does not // apply to them. if strings.HasPrefix(col, "extras.") { continue } if !qso.IsBulkEditable(col) && !qso.IsBulkEditableExtra(col) { t.Errorf("bulk-edit UI offers %q → column %q, which the repository refuses", id, col) } } } // Same contract for the filter builder: every field it offers must be one the // query layer will accept. func TestFilterFieldsAreQueryable(t *testing.T) { src, err := os.ReadFile("frontend/src/components/FilterBuilder.tsx") if err != nil { t.Skipf("frontend source not available: %v", err) } // Anchored on `type:` — the FIELDS entries carry one, the OPERATOR list does // not, and without it every operator (eq, contains, …) was read as a field. ids := regexp.MustCompile(`\{ value: '([a-z0-9_]+)', label: '[^']*', type:`).FindAllStringSubmatch(string(src), -1) if len(ids) < 10 { t.Fatalf("only %d field ids found — the parse is wrong, not the data", len(ids)) } for _, m := range ids { col := m[1] if !qso.IsFilterable(col) && !qso.IsFilterableExtra(col) { t.Errorf("filter builder offers %q, which the query layer refuses", col) } } }