Add js web stuff to landing page + documentation
This commit is contained in:
201
go/jsbundler/hmr_browser_test.go
Normal file
201
go/jsbundler/hmr_browser_test.go
Normal file
@@ -0,0 +1,201 @@
|
||||
//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 <div id="w">{msg()}</div>;
|
||||
}
|
||||
`)
|
||||
writeFile(t, filepath.Join(tmpSrc, "entry.tsx"),
|
||||
`import { render } from "solid-js/web";
|
||||
import Widget from "./Widget.js";
|
||||
render(() => <Widget/>, 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, `<!DOCTYPE html><html><head>
|
||||
<script>
|
||||
function beacon(p,t){fetch(p+'?text='+encodeURIComponent(t));}
|
||||
window.addEventListener('error', function(e){beacon('/__diag','ERR:'+((e.error&&e.error.stack)||e.message));});
|
||||
window.addEventListener('unhandledrejection', function(e){beacon('/__diag','REJ:'+((e.reason&&e.reason.stack)||String(e.reason)));});
|
||||
window.addEventListener('DOMContentLoaded', function(){beacon('/__diag','DOMCONTENTLOADED');});
|
||||
</script>
|
||||
<script type="importmap">%s</script>
|
||||
</head><body><div id="app"></div>
|
||||
<script type="module" src="/@src/entry.tsx"></script>
|
||||
</body></html>`, 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user