Commit 378df4415e
Verified · cmc
Layout: unified · split
e2e/formatpickerweb_test.go added +78
| @@ -0,0 +1,78 @@ | |||
| 1 | package e2e | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "net/url" | ||
| 5 | "os" | ||
| 6 | "path/filepath" | ||
| 7 | "strings" | ||
| 8 | "testing" | ||
| 9 | ) | ||
| 10 | |||
| 11 | // TestIssueMRWebFormat covers the format picker on the issue and merge | ||
| 12 | // request create forms (#160). Neither form offered --format before; a | ||
| 13 | // browser session could only ever write markdown. | ||
| 14 | func TestIssueMRWebFormat(t *testing.T) { | ||
| 15 | inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n") | ||
| 16 | aliceKey := inst.newKey(t, "alice") | ||
| 17 | inst.admin(t, "admin", "user", "create", "alice", | ||
| 18 | "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified") | ||
| 19 | if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 { | ||
| 20 | t.Fatalf("repo create: %s", errOut) | ||
| 21 | } | ||
| 22 | env := inst.gitEnv(aliceKey) | ||
| 23 | work := t.TempDir() | ||
| 24 | mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w") | ||
| 25 | dir := filepath.Join(work, "w") | ||
| 26 | mustGit(t, dir, env, "checkout", "-q", "-b", "main") | ||
| 27 | os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644) | ||
| 28 | mustGit(t, dir, env, "add", ".") | ||
| 29 | mustGit(t, dir, env, "commit", "-q", "-m", "base") | ||
| 30 | mustGit(t, dir, env, "push", "-q", "origin", "main") | ||
| 31 | mustGit(t, dir, env, "checkout", "-q", "-b", "topic") | ||
| 32 | os.WriteFile(filepath.Join(dir, "b.txt"), []byte("b\n"), 0o644) | ||
| 33 | mustGit(t, dir, env, "add", ".") | ||
| 34 | mustGit(t, dir, env, "commit", "-q", "-m", "topic") | ||
| 35 | mustGit(t, dir, env, "push", "-q", "origin", "topic") | ||
| 36 | |||
| 37 | alice := inst.login(t, aliceKey) | ||
| 38 | base := inst.base() + "/alice/app" | ||
| 39 | const orgBody = "Some /emphasis/ here.\n" | ||
| 40 | |||
| 41 | // The picker is on both forms, defaulting to Markdown. | ||
| 42 | for _, form := range []string{base + "/issues/new", base + "/mrs/new"} { | ||
| 43 | if _, body := browserGet(t, alice, form); !strings.Contains(body, `name="format"`) || | ||
| 44 | !strings.Contains(body, `<option value="md" selected>`) { | ||
| 45 | t.Fatalf("no format picker (defaulting to md) on %s:\n%s", form, body) | ||
| 46 | } | ||
| 47 | } | ||
| 48 | |||
| 49 | // An issue written with the org choice renders as org, not markdown. | ||
| 50 | if status, _ := browserPost(t, alice, base+"/issues/new", url.Values{ | ||
| 51 | "title": {"org issue"}, "body": {orgBody}, "format": {"org"}, | ||
| 52 | }); status != 200 { | ||
| 53 | t.Fatalf("issue create: %d", status) | ||
| 54 | } | ||
| 55 | if _, body := browserGet(t, alice, base+"/issues/1"); !strings.Contains(body, "<em>emphasis</em>") { | ||
| 56 | t.Fatalf("issue body did not render as org:\n%s", body) | ||
| 57 | } | ||
| 58 | |||
| 59 | // A merge request written with the org choice renders as org too. | ||
| 60 | if status, _ := browserPost(t, alice, base+"/mrs/new", url.Values{ | ||
| 61 | "source": {"topic"}, "target": {"main"}, "title": {"org mr"}, "body": {orgBody}, "format": {"org"}, | ||
| 62 | }); status != 200 { | ||
| 63 | t.Fatalf("mr create: %d", status) | ||
| 64 | } | ||
| 65 | if _, body := browserGet(t, alice, base+"/mrs/1"); !strings.Contains(body, "<em>emphasis</em>") { | ||
| 66 | t.Fatalf("mr body did not render as org:\n%s", body) | ||
| 67 | } | ||
| 68 | |||
| 69 | // A form left on the default choice still writes markdown. | ||
| 70 | if status, _ := browserPost(t, alice, base+"/issues/new", url.Values{ | ||
| 71 | "title": {"md issue"}, "body": {orgBody}, "format": {"md"}, | ||
| 72 | }); status != 200 { | ||
| 73 | t.Fatalf("issue create: %d", status) | ||
| 74 | } | ||
| 75 | if _, body := browserGet(t, alice, base+"/issues/2"); strings.Contains(body, "<em>emphasis</em>") { | ||
| 76 | t.Fatalf("issue body rendered as org despite the md choice:\n%s", body) | ||
| 77 | } | ||
| 78 | } | ||
internal/httpd/accounts.go +12 −2
| @@ -311,12 +311,17 @@ func (s *Server) issueCreateForm(w http.ResponseWriter, r *http.Request, u store | |||
| 311 | } | 311 | } |
| 312 | } | 312 | } |
| 313 | } | 313 | } |
| 314 | format := r.URL.Query().Get("format") | ||
| 315 | if format != "org" { | ||
| 316 | format = "md" | ||
| 317 | } | ||
| 314 | s.render(w, "issuenew.html", struct { | 318 | s.render(w, "issuenew.html", struct { |
| 315 | repoPage | 319 | repoPage |
| 316 | Body string | 320 | Body string |
| 321 | Format string | ||
| 317 | Template string | 322 | Template string |
| 318 | Templates []control.IssueTemplate | 323 | Templates []control.IssueTemplate |
| 319 | }{p, body, tplName, templates}) | 324 | }{p, body, format, tplName, templates}) |
| 320 | } | 325 | } |
| 321 | 326 | ||
| 322 | // Issue and merge request writes run the command the CLI runs, so the | 327 | // Issue and merge request writes run the command the CLI runs, so the |
| @@ -326,8 +331,13 @@ func (s *Server) issueCreateForm(w http.ResponseWriter, r *http.Request, u store | |||
| 326 | func (s *Server) issueCreateSubmit(w http.ResponseWriter, r *http.Request, u store.User) { | 331 | func (s *Server) issueCreateSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
| 327 | repoPath := r.PathValue("owner") + "/" + r.PathValue("repo") | 332 | repoPath := r.PathValue("owner") + "/" + r.PathValue("repo") |
| 328 | title := strings.TrimSpace(r.FormValue("title")) | 333 | title := strings.TrimSpace(r.FormValue("title")) |
| 334 | format := r.FormValue("format") | ||
| 335 | if format != "org" { | ||
| 336 | format = "md" | ||
| 337 | } | ||
| 329 | var created control.Created | 338 | var created control.Created |
| 330 | code, msg := s.dispatchIntoStdin(u, []string{"issue", "create", repoPath, "--title", title, "--file", "-"}, r.FormValue("body"), &created) | 339 | argv := []string{"issue", "create", repoPath, "--title", title, "--format", format, "--file", "-"} |
| 340 | code, msg := s.dispatchIntoStdin(u, argv, r.FormValue("body"), &created) | ||
| 331 | if code != protocol.ExitOK { | 341 | if code != protocol.ExitOK { |
| 332 | http.Error(w, msg, statusForExit(code)) | 342 | http.Error(w, msg, statusForExit(code)) |
| 333 | return | 343 | return |
internal/httpd/mractions.go +12 −3
| @@ -151,6 +151,7 @@ type mrNewPage struct { | |||
| 151 | Target string | 151 | Target string |
| 152 | Title string | 152 | Title string |
| 153 | Body string | 153 | Body string |
| 154 | Format string | ||
| 154 | Notice string | 155 | Notice string |
| 155 | } | 156 | } |
| 156 | 157 | ||
| @@ -166,10 +167,14 @@ func (s *Server) mrCreateForm(w http.ResponseWriter, r *http.Request, u store.Us | |||
| 166 | if target == "" { | 167 | if target == "" { |
| 167 | target = p.Repo.DefaultBranch | 168 | target = p.Repo.DefaultBranch |
| 168 | } | 169 | } |
| 170 | format := q.Get("format") | ||
| 171 | if format != "org" { | ||
| 172 | format = "md" | ||
| 173 | } | ||
| 169 | s.render(w, "mrnew.html", mrNewPage{ | 174 | s.render(w, "mrnew.html", mrNewPage{ |
| 170 | repoPage: p, Branches: branches, | 175 | repoPage: p, Branches: branches, |
| 171 | Source: q.Get("source"), Target: target, | 176 | Source: q.Get("source"), Target: target, |
| 172 | Title: q.Get("title"), Body: q.Get("body"), Notice: s.takeFlash(w, r), | 177 | Title: q.Get("title"), Body: q.Get("body"), Format: format, Notice: s.takeFlash(w, r), |
| 173 | }) | 178 | }) |
| 174 | } | 179 | } |
| 175 | 180 | ||
| @@ -182,9 +187,13 @@ func (s *Server) mrCreateSubmit(w http.ResponseWriter, r *http.Request, u store. | |||
| 182 | target := strings.TrimSpace(r.FormValue("target")) | 187 | target := strings.TrimSpace(r.FormValue("target")) |
| 183 | title := strings.TrimSpace(r.FormValue("title")) | 188 | title := strings.TrimSpace(r.FormValue("title")) |
| 184 | body := strings.TrimSpace(r.FormValue("body")) | 189 | body := strings.TrimSpace(r.FormValue("body")) |
| 190 | format := r.FormValue("format") | ||
| 191 | if format != "org" { | ||
| 192 | format = "md" | ||
| 193 | } | ||
| 185 | 194 | ||
| 186 | back := func(msg string) { | 195 | back := func(msg string) { |
| 187 | q := url.Values{"source": {source}, "target": {target}, "title": {title}, "body": {body}} | 196 | q := url.Values{"source": {source}, "target": {target}, "title": {title}, "body": {body}, "format": {format}} |
| 188 | s.setFlash(w, msg) | 197 | s.setFlash(w, msg) |
| 189 | http.Redirect(w, r, fmt.Sprintf("/%s/mrs/new?%s", p.Repo.Path(), q.Encode()), http.StatusSeeOther) | 198 | http.Redirect(w, r, fmt.Sprintf("/%s/mrs/new?%s", p.Repo.Path(), q.Encode()), http.StatusSeeOther) |
| 190 | } | 199 | } |
| @@ -192,7 +201,7 @@ func (s *Server) mrCreateSubmit(w http.ResponseWriter, r *http.Request, u store. | |||
| 192 | back("pick a source branch and give the merge request a title") | 201 | back("pick a source branch and give the merge request a title") |
| 193 | return | 202 | return |
| 194 | } | 203 | } |
| 195 | argv := []string{"mr", "create", p.Repo.Path(), "--source", source, "--title", title} | 204 | argv := []string{"mr", "create", p.Repo.Path(), "--source", source, "--title", title, "--format", format} |
| 196 | if target != "" { | 205 | if target != "" { |
| 197 | argv = append(argv, "--target", target) | 206 | argv = append(argv, "--target", target) |
| 198 | } | 207 | } |
internal/web/templates/issuenew.html +1
| @@ -6,6 +6,7 @@ | |||
| 6 | <form method="post" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/issues/new" class="commentform"> | 6 | <form method="post" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/issues/new" class="commentform"> |
| 7 | <p><input type="text" name="title" aria-label="Title" placeholder="title" required></p> | 7 | <p><input type="text" name="title" aria-label="Title" placeholder="title" required></p> |
| 8 | <p><textarea name="body" aria-label="Description" rows="12">{{.Body}}</textarea></p> | 8 | <p><textarea name="body" aria-label="Description" rows="12">{{.Body}}</textarea></p> |
| 9 | {{template "formatpicker" .Format}} | ||
| 9 | <p><input type="text" name="labels" aria-label="Labels" placeholder="labels, space-separated (write access)"></p> | 10 | <p><input type="text" name="labels" aria-label="Labels" placeholder="labels, space-separated (write access)"></p> |
| 10 | <p><button type="submit">Open issue</button></p> | 11 | <p><button type="submit">Open issue</button></p> |
| 11 | </form> | 12 | </form> |
internal/web/templates/layout.html +11
| @@ -122,6 +122,17 @@ | |||
| 122 | 122 | ||
| 123 | {{define "authorname"}}{{if .User}}<a class="authorlink" href="/{{.User}}" title="{{.Email}}">{{.Name}}</a>{{else}}<span title="{{.Email}}">{{.Name}}</span>{{end}}{{end}} | 123 | {{define "authorname"}}{{if .User}}<a class="authorlink" href="/{{.User}}" title="{{.Email}}">{{.Name}}</a>{{else}}<span title="{{.Email}}">{{.Name}}</span>{{end}}{{end}} |
| 124 | 124 | ||
| 125 | {{/* formatpicker is the md/org choice on a body: issue and MR create, and | ||
| 126 | a profile's about text. The argument is the currently selected | ||
| 127 | format; empty selects Markdown, today's default. */}} | ||
| 128 | {{define "formatpicker"}}<p class="branchpick"> | ||
| 129 | <label for="format">Body markup</label> | ||
| 130 | <select id="format" name="format"> | ||
| 131 | <option value="md"{{if ne . "org"}} selected{{end}}>Markdown</option> | ||
| 132 | <option value="org"{{if eq . "org"}} selected{{end}}>Org</option> | ||
| 133 | </select> | ||
| 134 | </p>{{end}} | ||
| 135 | |||
| 125 | {{/* difffiles renders a parsed diff: one foldable section per file, with | 136 | {{/* difffiles renders a parsed diff: one foldable section per file, with |
| 126 | line-number gutters and review threads inline. Base is the MR's | 137 | line-number gutters and review threads inline. Base is the MR's |
| 127 | endpoint and Viewer the signed-in account; the commit page passes | 138 | endpoint and Viewer the signed-in account; the commit page passes |
internal/web/templates/mrnew.html +1
| @@ -16,6 +16,7 @@ | |||
| 16 | </p> | 16 | </p> |
| 17 | <p><input type="text" name="title" aria-label="Title" placeholder="title" value="{{.Title}}" required></p> | 17 | <p><input type="text" name="title" aria-label="Title" placeholder="title" value="{{.Title}}" required></p> |
| 18 | <p><textarea name="body" aria-label="Description" rows="10" placeholder="what changes, and why">{{.Body}}</textarea></p> | 18 | <p><textarea name="body" aria-label="Description" rows="10" placeholder="what changes, and why">{{.Body}}</textarea></p> |
| 19 | {{template "formatpicker" .Format}} | ||
| 19 | <p><button type="submit">Open merge request</button></p> | 20 | <p><button type="submit">Open merge request</button></p> |
| 20 | </form> | 21 | </form> |
| 21 | <p class="meta">A branch in a fork opens from the CLI: | 22 | <p class="meta">A branch in a fork opens from the CLI: |