Add js web stuff to landing page + documentation
This commit is contained in:
203
go/jsbundler/ssr_test.go
Normal file
203
go/jsbundler/ssr_test.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package jsbundler
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// entryHome renders the fixture page the way ssrEntrySolid does: plain JS
|
||||
// (createComponent, no JSX in the entry) importing the .tsx page, which the Go
|
||||
// Solid compiler compiles. The layout is omitted to target the body.
|
||||
const entryHome = `
|
||||
import { render, createComponent } from "solid-js/web";
|
||||
import { Home } from "./frontend/src/pages/public/Home.tsx";
|
||||
globalThis.__render = function () {
|
||||
const root = document.createElement("div");
|
||||
const dispose = render(function () { return createComponent(Home, {}); }, root);
|
||||
const out = globalThis.__serialize(root);
|
||||
dispose();
|
||||
return out;
|
||||
};
|
||||
`
|
||||
|
||||
// homePage is the fixture entryHome imports: the smallest page that exercises the
|
||||
// thing under test — serverData() read inside the reactive body, a skeleton when
|
||||
// it is null, the data when it is not.
|
||||
//
|
||||
// It used to be cdrateline's real frontend/src/pages/public/Home.tsx, reached by
|
||||
// chdir'ing to "../..". That worked when the bundler lived inside cdrateline and
|
||||
// "../.." was the app root. Since the extraction, "../.." is the kjol repo root —
|
||||
// which has no frontend/ and, by the first golden rule, never will: the framework
|
||||
// does not import application code, and a test that does is the same coupling
|
||||
// wearing a different hat. Both tests here have been failing ever since.
|
||||
// It is deliberately not just a <div>: it carries the specific things the Solid
|
||||
// compiler has to get right when it bakes a template and goja serializes it — an
|
||||
// inline SVG (attribute CASE must survive: viewBox, not viewbox), a static string
|
||||
// style containing a url() with slashes and a comma-free but parenthesised value,
|
||||
// and an HTML entity.
|
||||
const homePage = `
|
||||
import { serverData } from "@kjol/ssr/serverData.ts";
|
||||
|
||||
interface Rate { term: string; low: string; high: string; avg: string }
|
||||
|
||||
const TERMS = ["90 Day", "180 Day", "1 Year"];
|
||||
|
||||
export const Home = () => {
|
||||
const data = () => serverData<{ rates: Rate[] }>();
|
||||
const rates = () => data()?.rates ?? [];
|
||||
return (
|
||||
<div class="page-home">
|
||||
<header style="background-image: url(/images/hero-bg.jpg)">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12h16" /></svg>
|
||||
<h1>Rates & terms</h1>
|
||||
</header>
|
||||
{rates().length === 0
|
||||
? <table class="animate-pulse">
|
||||
<tbody>{TERMS.map((t) => <tr><td>{t}</td><td>—</td></tr>)}</tbody>
|
||||
</table>
|
||||
: <table>
|
||||
<tbody>{rates().map((r) => (
|
||||
<tr><td>{r.term}</td><td>{r.low}</td><td>{r.high}</td><td>{r.avg}</td></tr>
|
||||
))}</tbody>
|
||||
</table>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`
|
||||
|
||||
// ssrFixtureRoot writes the fixture app tree to a temp dir, points the bundler at
|
||||
// it plus kjol's vendored Solid, and chdirs there — reproducing the server's setup
|
||||
// (cwd at the app root, relative "." project root) without needing a real app.
|
||||
func ssrFixtureRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
webAbs, err := filepath.Abs(filepath.Join("..", "jsruntime"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(webAbs, "runtime", "vendor.json")); err != nil {
|
||||
t.Skipf("kjol vendored solid runtime not present (%v)", err)
|
||||
}
|
||||
|
||||
root := t.TempDir()
|
||||
pageDir := filepath.Join(root, "frontend", "src", "pages", "public")
|
||||
if err := os.MkdirAll(pageDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(pageDir, "Home.tsx"), []byte(homePage), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Configure(Config{AppFrontend: filepath.Join(root, "frontend"), WebDir: webAbs})
|
||||
t.Chdir(root)
|
||||
return root
|
||||
}
|
||||
|
||||
// Proves the ISR data-injection path the server uses: a pre-bundled render entry
|
||||
// run in goja with server data injected (RenderBundleWithData) makes Home render
|
||||
// the injected rates (via serverData()) instead of the loading skeleton.
|
||||
func TestRenderHomeWithServerData(t *testing.T) {
|
||||
ssrFixtureRoot(t)
|
||||
|
||||
data := `{"rates":[{"term":"90 Day","low":"1.111%","high":"2.222%","avg":"1.500%"}]}`
|
||||
js, err := BundleEntry(entryHome, ".")
|
||||
if err != nil {
|
||||
t.Fatalf("bundle: %v", err)
|
||||
}
|
||||
out, err := RenderBundleWithData(js, data)
|
||||
if err != nil {
|
||||
t.Fatalf("render with data: %v", err)
|
||||
}
|
||||
for _, want := range []string{"1.111%", "2.222%", "1.500%"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("output missing injected rate %q", want)
|
||||
}
|
||||
}
|
||||
// With data present the table shows it, not the loading skeleton.
|
||||
if strings.Contains(out, "animate-pulse") {
|
||||
t.Errorf("skeleton rendered despite injected data")
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduces the server's setup (cwd at the app root, relative "." project root).
|
||||
// Guards the esbuild alias-resolution bug where a non-absolute root yields
|
||||
// bare-specifier alias targets that fail to resolve.
|
||||
func TestRenderWithRelativeRoot(t *testing.T) {
|
||||
ssrFixtureRoot(t)
|
||||
|
||||
r := NewRenderer(".", entryHome, true)
|
||||
out, err := r.HTML()
|
||||
if err != nil {
|
||||
t.Fatalf("render with relative root: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, `class="page-home"`) {
|
||||
t.Errorf("output missing page-home")
|
||||
}
|
||||
}
|
||||
|
||||
// Confirms the cache: the first HTML() pays bundle+render, the second (no source
|
||||
// change) returns the cached string near-instantly.
|
||||
func TestRendererCaching(t *testing.T) {
|
||||
root := ssrFixtureRoot(t)
|
||||
r := NewRenderer(root, entryHome, false) // prod: warm HTML() returns the cached string directly
|
||||
|
||||
t0 := time.Now()
|
||||
first, err := r.HTML()
|
||||
if err != nil {
|
||||
t.Fatalf("cold render: %v", err)
|
||||
}
|
||||
cold := time.Since(t0)
|
||||
|
||||
t1 := time.Now()
|
||||
second, err := r.HTML()
|
||||
if err != nil {
|
||||
t.Fatalf("warm render: %v", err)
|
||||
}
|
||||
warm := time.Since(t1)
|
||||
t.Logf("cold=%v warm=%v", cold, warm)
|
||||
|
||||
if first != second {
|
||||
t.Errorf("cached output differs from first render")
|
||||
}
|
||||
if warm > cold/4 {
|
||||
t.Errorf("cache hit too slow: cold=%v warm=%v", cold, warm)
|
||||
}
|
||||
}
|
||||
|
||||
// No data → the page renders its loading skeleton, exercising compiled Solid
|
||||
// against inline SVGs (viewBox case), string style attributes, entities, and the
|
||||
// table. SVG attribute case and the style string must survive serialization.
|
||||
func TestRenderHomeSkeleton(t *testing.T) {
|
||||
root := ssrFixtureRoot(t)
|
||||
|
||||
bundle, err := BundleEntry(entryHome, root)
|
||||
if err != nil {
|
||||
t.Fatalf("bundle: %v", err)
|
||||
}
|
||||
eng, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("engine: %v", err)
|
||||
}
|
||||
if err := eng.LoadBundle(bundle); err != nil {
|
||||
t.Fatalf("load bundle: %v", err)
|
||||
}
|
||||
out, err := eng.Render()
|
||||
if err != nil {
|
||||
t.Fatalf("render: %v", err)
|
||||
}
|
||||
t.Logf("HOME SKELETON (%d bytes):\n%s", len(out), out)
|
||||
|
||||
for _, want := range []string{
|
||||
`class="page-home"`,
|
||||
`viewBox="0 0 24 24"`, // SVG attribute case preserved
|
||||
`background-image: url(/images/hero-bg.jpg)`, // static string style baked into the template
|
||||
`animate-pulse`, // skeleton bars (no server data => skeleton branch)
|
||||
`90 Day`, // term labels shown in the skeleton
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("output missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user