//go:build dev package jsbundler import ( "context" "fmt" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "runtime" "strings" "sync" "testing" "time" ) // chromePath returns a headless-capable Chrome binary, or "" if none is found. func chromePath() string { candidates := []string{ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium", } for _, name := range []string{"google-chrome", "chromium", "chromium-browser", "google-chrome-stable"} { if p, err := exec.LookPath(name); err == nil { candidates = append(candidates, p) } } for _, c := range candidates { if _, err := os.Stat(c); err == nil { return c } } return "" } // TestBrowserNativeESMRenders proves the whole native-ESM path works in a real // browser: the import map resolves solid-js to one vendored instance, the module // server transforms + serves the entry and a .tsx component (solid-refresh // compiled), and Solid mounts it reactively. The page reports its rendered text // back to the test via a beacon (robust against headless Chrome's one-shot exit // quirks). It does NOT assert the interactive hot-swap — that needs a persistent // CDP session and is the final manual check — but removes the largest // browser-integration risk. func TestBrowserNativeESMRenders(t *testing.T) { chrome := chromePath() if chrome == "" { t.Skip("no headless Chrome/Chromium available") } if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { t.Skip("browser smoke test only wired for macOS/Linux") } root, err := filepath.Abs("../..") if err != nil { t.Fatal(err) } t.Chdir(root) frontendAbs, _ := filepath.Abs(frontendDir) // Fixtures in a temp src root so the real source tree (and Tailwind scan / // route gen) is untouched. The component uses JSX (→ solid-refresh) and a // signal (→ real reactivity); the entry imports it via a `.js` specifier // (→ .tsx resolution), then beacons back the rendered text. tmpSrc := evalSymlinks(t.TempDir()) writeFile(t, filepath.Join(tmpSrc, "Widget.tsx"), `import { createSignal } from "solid-js"; export default function Widget() { const [msg] = createSignal("hello-hmr-rendered"); return
{msg()}
; } `) writeFile(t, filepath.Join(tmpSrc, "entry.tsx"), `import { render } from "solid-js/web"; import Widget from "./Widget.js"; render(() => , document.getElementById("app")); setTimeout(() => { const el = document.getElementById("w"); fetch("/__result?text=" + encodeURIComponent(el ? el.textContent : "EMPTY")); }, 0); `) eps, err := loadVendorManifest(filepath.Join(frontendAbs, "vendor")) if err != nil { t.Fatalf("vendor manifest: %v", err) } d := &devServer{ hub: newHub(), frontend: frontendAbs, srcRoot: tmpSrc, vendor: filepath.Join(frontendAbs, "vendor"), graph: newModuleGraph(), vendorCache: map[string][]byte{}, cssTrigger: make(chan struct{}, 1), vendorEntrypoints: eps, } d.importMapJSON = d.buildImportMap() rendered := make(chan string, 1) var diagMu sync.Mutex var diag []string mux := http.NewServeMux() d.register(mux) mux.HandleFunc("/__result", func(w http.ResponseWriter, r *http.Request) { select { case rendered <- r.URL.Query().Get("text"): default: } w.WriteHeader(http.StatusNoContent) }) mux.HandleFunc("/__diag", func(w http.ResponseWriter, r *http.Request) { diagMu.Lock() diag = append(diag, r.URL.Query().Get("text")) diagMu.Unlock() w.WriteHeader(http.StatusNoContent) }) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, `
`, d.importMapJSON) }) srv := httptest.NewServer(mux) defer srv.Close() // Sanity-check that every module the page needs serves (200) before the browser. for _, m := range []string{"/@src/entry.tsx", "/@src/Widget.tsx", "/@hmr/client", vendorURLPrefix + "solid-js.js", vendorURLPrefix + "solid-js/web.js"} { resp, err := http.Get(srv.URL + m) if err != nil { t.Fatalf("GET %s: %v", m, err) } resp.Body.Close() if resp.StatusCode != 200 { t.Fatalf("GET %s -> %d", m, resp.StatusCode) } } // Launch headless Chrome to load the page; wait for the render beacon rather // than for Chrome to exit (new headless doesn't reliably one-shot-exit here). // Use a best-effort temp profile dir (not t.TempDir): Chrome's detached helper // processes may still be writing to it at cleanup time, and t.TempDir's strict // RemoveAll would then fail the test. userDir, _ := os.MkdirTemp("", "hmr-chrome-*") defer os.RemoveAll(userDir) devNull, _ := os.Open(os.DevNull) defer devNull.Close() ctx, cancel := context.WithCancel(context.Background()) cmd := exec.CommandContext(ctx, chrome, "--headless=new", "--disable-gpu", "--no-sandbox", "--no-first-run", "--user-data-dir="+userDir, "--disable-background-networking", "--disable-component-update", "--disable-default-apps", "--disable-sync", "--no-default-browser-check", srv.URL, ) cmd.Stdout = devNull cmd.Stderr = devNull if err := cmd.Start(); err != nil { cancel() t.Fatalf("start chrome: %v", err) } defer func() { cancel(); cmd.Wait() }() select { case text := <-rendered: if text != "hello-hmr-rendered" { t.Fatalf("browser rendered %q, want %q", text, "hello-hmr-rendered") } t.Log("native-ESM app rendered in headless Chrome (import map + single solid-js + solid-refresh component)") case <-time.After(25 * time.Second): diagMu.Lock() msgs := strings.Join(diag, "\n ") diagMu.Unlock() t.Fatalf("timed out waiting for the browser render beacon.\nbrowser diagnostics:\n %s", msgs) } } func writeFile(t *testing.T, path, content string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } }