···2323- Prefer simple html to javascript frameworks
24242525## Testing Guidelines
2626+- Always run tests after making code changes. Default to `go test ./...`; use a narrower `go test ./... -run TestName` only when appropriate for quick iteration. If you cannot run tests, explicitly say why.
2627- Place tests alongside code in `*_test.go`; prefer table-driven cases and explicit fixtures over implicit globals.
2728- Use `go test ./... -run TestName` for targeted debugging; keep deterministic by avoiding network calls and using fakes where possible.
2829- When touching recipe generation or Kroger client code, add assertions that cover API shape changes and template output (see existing tests in `internal/recipes` and `internal/html`).
+39-1
cmd/careme/web_e2e_test.go
···1717 "careme/internal/locations"
1818 "careme/internal/recipes"
1919 "careme/internal/users"
2020+2121+ "golang.org/x/net/html"
2022)
21232224func TestWebEndToEndFlowWithMocks(t *testing.T) {
···4648 }
47494850 savedHash := recipeHashes[0]
5151+ // Step 2b: load a shared recipe page directly.
5252+ _ = mustGetBody(t, client, srv.URL+"/recipe/"+url.PathEscape(savedHash))
4953 dismissedHashes := recipeHashes[1:3]
50545155 //step 4 todo regenrate again with commentary then save two more
···171175 body := readAll(t, resp.Body)
172176 t.Fatalf("GET %s expected 200, got %d: %s", url, resp.StatusCode, body)
173177 }
174174- return readAll(t, resp.Body)
178178+ body := readAll(t, resp.Body)
179179+ requireValidHTML(t, url, resp.Header.Get("Content-Type"), body)
180180+ return body
175181}
176182177183func followUntilRecipes(t *testing.T, client *http.Client, startURL string, expectSpinner bool) (string, string) {
···278284 }
279285 return string(data)
280286}
287287+288288+func requireValidHTML(t *testing.T, url, contentType, body string) {
289289+ t.Helper()
290290+ if strings.TrimSpace(body) == "" {
291291+ t.Fatalf("GET %s returned empty body", url)
292292+ }
293293+ if contentType != "" && !strings.Contains(strings.ToLower(contentType), "text/html") {
294294+ t.Fatalf("GET %s expected HTML content-type, got %q", url, contentType)
295295+ }
296296+ if !strings.Contains(strings.ToLower(body), "<html") {
297297+ t.Fatalf("GET %s expected HTML body, missing <html> tag", url)
298298+ }
299299+ doc, err := html.Parse(strings.NewReader(body))
300300+ if err != nil {
301301+ t.Fatalf("GET %s returned invalid HTML: %v", url, err)
302302+ }
303303+ if !hasElement(doc, "body") {
304304+ t.Fatalf("GET %s expected HTML body element", url)
305305+ }
306306+}
307307+308308+func hasElement(n *html.Node, name string) bool {
309309+ if n.Type == html.ElementNode && n.Data == name {
310310+ return true
311311+ }
312312+ for child := n.FirstChild; child != nil; child = child.NextSibling {
313313+ if hasElement(child, name) {
314314+ return true
315315+ }
316316+ }
317317+ return false
318318+}