diff --git a/Makefile b/Makefile index 50d9e5f..e909908 100644 --- a/Makefile +++ b/Makefile @@ -7,13 +7,13 @@ FRONTEND_PORT ?= 3000 ## Compile both tiers plus the ledger import tool through HowlFrame. build: mkdir -p build - $(HOWLFRAME_BIN) frontend/app.howl -o frontend + $(HOWLFRAME_BIN) build frontend/app.howl --target=js -o frontend $(HOWLFRAME_BIN) -compile-bc backend/server.howl -o backend/server.hfbc $(HOWLFRAME_BIN) -compile-bc tools/ledger_import/ledger_import.howl -o build/ledger_import.hfbc @# The published showcase is compiled from .howl too, importing the same @# rendering module as the interface. The JS backend names its output @# app.js, so it is renamed to sit beside the site's own script.js. - $(HOWLFRAME_BIN) docs/demo.howl -o docs + $(HOWLFRAME_BIN) build docs/demo.howl --target=js -o docs mv docs/app.js docs/demo.js ## Black-box contract tests against the real compiled bytecode server. diff --git a/README.md b/README.md index 933cc53..db27b92 100644 --- a/README.md +++ b/README.md @@ -171,3 +171,24 @@ reuses their vocabulary rather than inventing a parallel one — see ## License MIT. See [LICENSE](LICENSE). + +## Browser compatibility and smoke check + +The frontend and published demo compile with `build --target=js`; bare-path +compilation in current HowlFrame selects standalone bytecode. The hosting page +grants only `network` through `globalThis.HOWLFRAME_ALLOW_CAPS` before loading +the compiled frontend. This requires the HowlFrame browser host-grant fix; +older binaries can compile but cannot complete browser network calls. + +For a read-only browser check, run an isolated API and frontend using the +quickstart above. Optionally load demo missions with `make seed`. With Python +Playwright and its Chromium runtime installed, run: + +```bash +python3 scripts/test_browser.py --output /tmp/howlboard-browser-evidence +``` + +The check verifies real API loading, desktop/mobile overflow, keyboard focus, +and browser errors, and saves screenshots. It creates no missions or approvals. +It is a smoke check, not a complete accessibility audit. Backend contract +tests remain the gate for state transitions and authority. diff --git a/backend/contract_test.go b/backend/contract_test.go index f2daa31..70c87a3 100644 --- a/backend/contract_test.go +++ b/backend/contract_test.go @@ -435,6 +435,30 @@ func TestMissionAPIContract(t *testing.T) { } }) + t.Run("dependency scalar types have actionable errors and null is empty", func(t *testing.T) { + for _, value := range []string{`""`, `17`, `true`, `false`} { + status, body := post(t, "/api/missions/create", + fmt.Sprintf(`{"title":"Scalar deps","project":"HowlBoard","depends_on":%s}`, value)) + if status != http.StatusBadRequest || body["error"] != "INVALID_DEPENDS_ON" || body["reason"] != "not_a_list" || body["field"] != "depends_on" { + t.Errorf("dependency value %s = %d %v, want actionable 400", value, status, body) + } + } + status, body := post(t, "/api/missions/create", + `{"title":"Null deps","project":"HowlBoard","depends_on":null}`) + if status != http.StatusCreated { + t.Fatalf("null dependencies = %d %v, want 201", status, body) + } + deps, ok := body["depends_on"].([]any) + if !ok || len(deps) != 0 { + t.Fatalf("null dependencies = %v, want empty list", body["depends_on"]) + } + status, _ = post(t, "/api/missions/delete", + fmt.Sprintf(`{"id":%q}`, body["id"].(string))) + if status != http.StatusOK { + t.Fatalf("null dependency fixture cleanup = %d", status) + } + }) + // store_keys enumerates the store directly, so a delete must disappear from // listings with no separate index record to keep in step. t.Run("create and delete round-trip through store enumeration", func(t *testing.T) { @@ -689,7 +713,7 @@ func TestDependsOnRender(t *testing.T) { root := repoRoot(t) outDir := t.TempDir() bin := filepath.Join(root, "howlframe_bin") - cmd := exec.Command(bin, filepath.Join(root, "frontend", "render_harness.howl"), "-o", outDir) + cmd := exec.Command(bin, "build", filepath.Join(root, "frontend", "render_harness.howl"), "--target=js", "-o", outDir) cmd.Dir = root if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("compile render harness: %v\n%s", err, output) @@ -1510,7 +1534,7 @@ func TestFactoryViewEscapes(t *testing.T) { root := repoRoot(t) outDir := t.TempDir() bin := filepath.Join(root, "howlframe_bin") - cmd := exec.Command(bin, filepath.Join(root, "frontend", "factory_harness.howl"), "-o", outDir) + cmd := exec.Command(bin, "build", filepath.Join(root, "frontend", "factory_harness.howl"), "--target=js", "-o", outDir) cmd.Dir = root if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("compile factory harness: %v\n%s", err, output) diff --git a/backend/server.howl b/backend/server.howl index 207fd50..fd4a481 100644 --- a/backend/server.howl +++ b/backend/server.howl @@ -190,9 +190,14 @@ (defun validate_depends_on (raw) (type_hint raw "any") (type_hint return "dict") - (if (= (to_string raw) "") + (if (is_nil raw) (return (dict ("ok" "true") ("deps" (list)))) - (try_let (n (list_len raw)) + (if (= raw (to_string raw)) + (return (dict + ("ok" "false") + ("error" "INVALID_DEPENDS_ON") + ("reason" "not_a_list"))) + (try_let (n (list_len raw)) (catch err (return (dict ("ok" "false") ("error" "INVALID_DEPENDS_ON") @@ -217,6 +222,7 @@ ) ) ) + ) ) ) @@ -1173,7 +1179,9 @@ (if (= prio "") (set prio "NORMAL") (do)) (if (and (!= prio "LOW") (and (!= prio "NORMAL") (!= prio "HIGH"))) (res_json 400 (dict ("error" "INVALID_PRIORITY") ("valid" "LOW, NORMAL, HIGH"))) - (let (dep_check (call validate_depends_on (map_get body "depends_on"))) + (let (dep_check (if (map_has body "depends_on") + (call validate_depends_on (map_get body "depends_on")) + (dict ("ok" "true") ("deps" (list))))) (if (!= (map_get dep_check "ok") "true") (res_json 400 (dict ("error" (map_get dep_check "error")) diff --git a/change_log.md b/change_log.md new file mode 100644 index 0000000..05a0ed8 --- /dev/null +++ b/change_log.md @@ -0,0 +1,8 @@ +# Changelog + +## HOWL-E2E-UX-001 + +- Compile browser targets explicitly and declare only the frontend network grant. +- Keep malformed dependency inputs at HTTP 400 under the current runtime. +- Wrap long Factory blocker reasons on mobile. +- Add a read-only desktop/mobile browser smoke that detects real API failures. diff --git a/docs/demo.js b/docs/demo.js index 43684b0..822ca8d 100644 --- a/docs/demo.js +++ b/docs/demo.js @@ -1,5 +1,11 @@ function howlFrameGrantHas(name) { - var raw = process.env.HOWLFRAME_ALLOW_CAPS || ""; + var raw = ""; + if (typeof process !== "undefined" && process.env) { + raw = process.env.HOWLFRAME_ALLOW_CAPS || ""; + } else if (typeof globalThis !== "undefined") { + raw = globalThis.HOWLFRAME_ALLOW_CAPS; + } + if (typeof raw !== "string") return false; var parts = raw.split(","); for (var i = 0; i < parts.length; i++) { if (parts[i].trim() === name) return true; @@ -1449,5 +1455,9 @@ let first = howlFrameListGet(missions, 0); })().catch((err) => { console.error(err && err.message ? err.message : err); - process.exit(1); + if (typeof process !== "undefined" && typeof process.exit === "function") { + process.exit(1); + } else { + throw err; + } }); diff --git a/docs/index.html b/docs/index.html index aae6583..97cbd20 100644 --- a/docs/index.html +++ b/docs/index.html @@ -563,6 +563,7 @@