| 1 | package savestatus |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "html/template" |
| 6 | "os/exec" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | func render(t *testing.T, data Data) string { |
| 12 | t.Helper() |
| 13 | view, err := template.New("test").Parse(Template()) |
| 14 | if err != nil { |
| 15 | t.Fatal(err) |
| 16 | } |
| 17 | var b bytes.Buffer |
| 18 | if err := view.ExecuteTemplate(&b, "estilo-save-status", data); err != nil { |
| 19 | t.Fatal(err) |
| 20 | } |
| 21 | return b.String() |
| 22 | } |
| 23 | |
| 24 | func TestRenderStatesAndSilentUnknown(t *testing.T) { |
| 25 | for _, tc := range []struct{ state, want string }{ |
| 26 | {"saved", "gespeichert"}, {"saving", "guardando"}, |
| 27 | {"retrying", "再試行"}, {"failed", "فشل"}, |
| 28 | {"idle", ""}, {"unsupported", ""}, |
| 29 | } { |
| 30 | t.Run(tc.state, func(t *testing.T) { |
| 31 | got := render(t, Data{State: tc.state, Saved: "gespeichert", Saving: "guardando", Retrying: "再試行", Failed: "فشل"}) |
| 32 | if !strings.HasSuffix(got, ">"+tc.want+"</span>") { |
| 33 | t.Fatalf("wrong initial text: %s", got) |
| 34 | } |
| 35 | for _, attr := range []string{`role="status"`, `aria-live="polite"`, `aria-atomic="true"`, `estilo-save-status="inline"`} { |
| 36 | if !strings.Contains(got, attr) { |
| 37 | t.Errorf("missing %s in %s", attr, got) |
| 38 | } |
| 39 | } |
| 40 | if strings.Contains(got, "aria-hidden") || strings.Contains(got, " hidden") || strings.Contains(got, "aria-label") { |
| 41 | t.Errorf("silent or unnamed region changed semantics: %s", got) |
| 42 | } |
| 43 | if tc.want == "" && !strings.Contains(got, `data-state="idle"`) { |
| 44 | t.Errorf("unknown state was not normalized: %s", got) |
| 45 | } |
| 46 | }) |
| 47 | } |
| 48 | got := render(t, Data{State: "failed", Placement: "floating"}) |
| 49 | if !strings.HasSuffix(got, "></span>") || !strings.Contains(got, `estilo-save-status="floating"`) { |
| 50 | t.Fatalf("missing state text must stay empty: %s", got) |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | func TestLabelsAndStatusTextAreEscaped(t *testing.T) { |
| 55 | unsafe := `"><script>globalThis.fixture=1</script>&` |
| 56 | got := render(t, Data{State: "failed", Failed: unsafe, Label: unsafe, Placement: unsafe}) |
| 57 | if strings.Contains(got, "<script>") || strings.Count(got, "<span ") != 1 || strings.Count(got, "</span>") != 1 { |
| 58 | t.Fatalf("text became markup: %s", got) |
| 59 | } |
| 60 | if !strings.Contains(got, `estilo-save-status="inline"`) || !strings.Contains(got, `aria-label=""><script>`) || !strings.HasSuffix(got, "</script>&</span>") { |
| 61 | t.Fatalf("attribute or text escaping failed: %s", got) |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | func TestJavaScriptStateTransitions(t *testing.T) { |
| 66 | output, err := exec.Command("node", "--test", "status.test.mjs").CombinedOutput() |
| 67 | if err != nil { |
| 68 | t.Fatalf("status module: %v\n%s", err, output) |
| 69 | } |
| 70 | } |
| 71 | |