diff --git a/CLAUDE.md b/CLAUDE.md index 64f91af..b86bde7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,3 +67,20 @@ When a form is submitted, the framework resolves the action in this order: - `profile-progressive/` — Simple Tier 1 form with validation - `live-preview/` — Tier 1 with `Change()` method for live updates - `chat/` — Tier 1+2 (uses `lvt-scroll` for auto-scroll) + +## CSS + +All examples use [Pico CSS](https://picocss.com/docs) exclusively: + +- Include via CDN: `` +- Use semantic HTML — Pico auto-styles: `
` (cards), `` (modals), `
` (accordions), ``, `
+ + {{range .Items}} + + + + + {{end}} + +
{{.}} +
+ + +
+
+ {{else}} + No items. Add some above! + {{end}} +
+ + +
+ About Flash Messages: + +
+ {{if .lvt.DevMode}} diff --git a/flash-messages/flash_test.go b/flash-messages/flash_test.go index f2152f6..cfba096 100644 --- a/flash-messages/flash_test.go +++ b/flash-messages/flash_test.go @@ -59,9 +59,9 @@ func TestFlash_ShowsInTemplate(t *testing.T) { t.Errorf("Expected success flash 'Added item: Test Item' in response, got:\n%s", body) } - // Should have flash-success class - if !strings.Contains(body, "flash-success") { - t.Errorf("Expected flash-success class in response") + // Should have data-flash="success" attribute + if !strings.Contains(body, `data-flash="success"`) { + t.Errorf("Expected data-flash=\"success\" attribute in response") } } @@ -149,36 +149,36 @@ func TestFlash_DifferentTypes(t *testing.T) { name string action string item string - wantClass string + wantAttr string wantText string }{ { - name: "success flash", - action: "addItem", - item: "New Item", - wantClass: "flash-success", - wantText: "Added item: New Item", + name: "success flash", + action: "addItem", + item: "New Item", + wantAttr: `data-flash="success"`, + wantText: "Added item: New Item", }, { - name: "warning flash (duplicate)", - action: "addItem", - item: "New Item", // Same item = duplicate - wantClass: "flash-warning", - wantText: "Item already exists", + name: "warning flash (duplicate)", + action: "addItem", + item: "New Item", // Same item = duplicate + wantAttr: `data-flash="warning"`, + wantText: "Item already exists", }, { - name: "info flash", - action: "removeItem", - item: "New Item", - wantClass: "flash-info", - wantText: "Removed item: New Item", + name: "info flash", + action: "removeItem", + item: "New Item", + wantAttr: `data-flash="info"`, + wantText: "Removed item: New Item", }, { - name: "error flash", - action: "simulateError", - item: "", - wantClass: "flash-error", - wantText: "Something went wrong", + name: "error flash", + action: "simulateError", + item: "", + wantAttr: `data-flash="error"`, + wantText: "Something went wrong", }, } @@ -197,8 +197,8 @@ func TestFlash_DifferentTypes(t *testing.T) { body := readBody(t, resp) resp.Body.Close() - if !strings.Contains(body, tt.wantClass) { - t.Errorf("Expected %s in response", tt.wantClass) + if !strings.Contains(body, tt.wantAttr) { + t.Errorf("Expected %s in response", tt.wantAttr) } if !strings.Contains(body, tt.wantText) { t.Errorf("Expected '%s' in response, got:\n%s", tt.wantText, body) @@ -243,8 +243,8 @@ func TestFlash_FieldErrorsStillWork(t *testing.T) { if !strings.Contains(body, "Item name is required") { t.Error("Expected field error 'Item name is required'") } - if !strings.Contains(body, "field-error") { - t.Error("Expected field-error class") + if !strings.Contains(body, "Item name is required") { + t.Error("Expected field error in element") } } diff --git a/go.mod b/go.mod index be164aa..c6a8ce2 100644 --- a/go.mod +++ b/go.mod @@ -15,10 +15,21 @@ require ( ) require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/brianvoe/gofakeit/v7 v7.8.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/bubbles v0.21.0 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/chromedp/sysutil v1.1.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -28,20 +39,35 @@ require ( github.com/gobwas/ws v1.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/pressly/goose/v3 v3.26.0 // indirect github.com/redis/go-redis/v9 v9.17.2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect github.com/tdewolff/minify/v2 v2.24.8 // indirect github.com/tdewolff/parse/v2 v2.8.5 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect + golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.50.0 // indirect + golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.14.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.67.4 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index 1c64aea..5c0c001 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,12 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/brianvoe/gofakeit/v7 v7.8.2 h1:FWxoSP4Ss9LWSvTOrWZHz7sIHcpZwLVw2xa/DhJABB4= +github.com/brianvoe/gofakeit/v7 v7.8.2/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -12,6 +18,20 @@ github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqy github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU= github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM= @@ -45,6 +65,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= @@ -98,14 +120,22 @@ github.com/livetemplate/lvt v0.0.0-20260327182801-53d6d40e692e h1:nAV7BaOatFcbSa github.com/livetemplate/lvt v0.0.0-20260327182801-53d6d40e692e/go.mod h1:17cFl500ntymD3gx8h+ZODnVnTictHgG8Wmz/By75sU= github.com/livetemplate/lvt/components v0.0.0-20260327182801-53d6d40e692e h1:vuR0pQtEQHZOD2/HvTJfHPKEdoD77XUs1mq1kdjgVig= github.com/livetemplate/lvt/components v0.0.0-20260327182801-53d6d40e692e/go.mod h1:G9PElN3LRf8xoRtoxbOAcTkV/4FhrCE/Laczkz5bfL4= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= @@ -126,6 +156,12 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -140,10 +176,17 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= +github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -166,6 +209,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -178,6 +223,8 @@ go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/Wgbsd go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= @@ -188,6 +235,7 @@ golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= @@ -197,6 +245,7 @@ golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= diff --git a/graceful-shutdown/counter.tmpl b/graceful-shutdown/counter.tmpl index db32af0..75f25af 100644 --- a/graceful-shutdown/counter.tmpl +++ b/graceful-shutdown/counter.tmpl @@ -4,20 +4,25 @@ {{.Title}} + -

{{.Title}}

-
-

Counter: {{.Counter}}

-
- - - -
-
-
-

Last updated: {{.LastUpdated}}

-
+
+
+
+

{{.Title}}

+
+

Counter: {{.Counter}}

+
+ + + +
+
+ Last updated: {{.LastUpdated}} +
+
+
{{if .lvt.DevMode}} diff --git a/live-preview/live_preview_test.go b/live-preview/live_preview_test.go index 95a4e68..18b8119 100644 --- a/live-preview/live_preview_test.go +++ b/live-preview/live_preview_test.go @@ -219,6 +219,27 @@ func TestLivePreviewE2E(t *testing.T) { } }) + t.Run("Live_Input_Updates_Preview", func(t *testing.T) { + // Type in the name field — Change() method auto-fires with 300ms debounce + err := chromedp.Run(ctx, + chromedp.Clear(`input[name="Name"]`, chromedp.ByQuery), + chromedp.SendKeys(`input[name="Name"]`, "Alice", chromedp.ByQuery), + e2etest.WaitFor(`document.querySelector('#preview').textContent.includes('Hello, Alice!')`, 5*time.Second), + ) + if err != nil { + t.Fatalf("Failed to type and see preview update: %v", err) + } + + var preview string + if err := chromedp.Run(ctx, chromedp.Text(`#preview`, &preview, chromedp.ByQuery)); err != nil { + t.Fatalf("Failed to get preview text: %v", err) + } + if !strings.Contains(preview, "Hello, Alice!") { + t.Errorf("Expected preview 'Hello, Alice!', got %q", preview) + } + t.Log("✅ Live input updates preview in real-time") + }) + t.Run("WebSocket Connection", func(t *testing.T) { err := chromedp.Run(ctx, e2etest.WaitFor(`typeof window.liveTemplateClient !== 'undefined'`, 3*time.Second), diff --git a/live-preview/preview.tmpl b/live-preview/preview.tmpl index 445fd95..f7844c2 100644 --- a/live-preview/preview.tmpl +++ b/live-preview/preview.tmpl @@ -4,15 +4,23 @@ Live Preview + -

Live Preview

-
- - - -
-
{{.Preview}}
+
+
+
+

Live Preview

+
+
+ + +
+
{{.Preview}}
+
+
{{if .lvt.DevMode}} diff --git a/login/templates/auth.html b/login/templates/auth.html index 3224ccd..a394521 100644 --- a/login/templates/auth.html +++ b/login/templates/auth.html @@ -4,171 +4,57 @@ Login Example - LiveTemplate - - {{if .lvt.DevMode}} - - {{else}} - - {{end}} + -
+
{{if .IsLoggedIn}} -

Dashboard

- {{if .ServerMessage}} -
- {{.ServerMessage}} -
- {{end}} -
-

Welcome, {{.Username}}!

-

You are successfully logged in.

- {{if not .LoginTime.IsZero}} - +
+

Dashboard

+ {{if .ServerMessage}} + {{.ServerMessage}} {{end}} -
- -
- -
+
+

Welcome, {{.Username}}!

+

You are successfully logged in.

+ {{if not .LoginTime.IsZero}} + Logged in at: {{.LoginTime.Format "15:04:05"}} + {{end}} +
+ +
+ +
+ {{else}} -

Login

- {{if .Error}} -
{{.Error}}
- {{end}} - -
-
- - -
-
- - -
- -
- -
- Demo: Use any username with password secret
- After login, the server will push a welcome message via WebSocket. -
+
+

Login

+ {{if .Error}} + {{.Error}} + {{end}} + +
+ + + +
+ + Demo: Use any username with password secret
+ After login, the server will push a welcome message via WebSocket. +
+
{{end}} -
+ + {{if .lvt.DevMode}} + + {{else}} + + {{end}} diff --git a/observability/counter.tmpl b/observability/counter.tmpl index db32af0..75f25af 100644 --- a/observability/counter.tmpl +++ b/observability/counter.tmpl @@ -4,20 +4,25 @@ {{.Title}} + -

{{.Title}}

-
-

Counter: {{.Counter}}

-
- - - -
-
-
-

Last updated: {{.LastUpdated}}

-
+
+
+
+

{{.Title}}

+
+

Counter: {{.Counter}}

+
+ + + +
+
+ Last updated: {{.LastUpdated}} +
+
+
{{if .lvt.DevMode}} diff --git a/production/single-host/app.tmpl b/production/single-host/app.tmpl index d6eade7..6a9212a 100644 --- a/production/single-host/app.tmpl +++ b/production/single-host/app.tmpl @@ -4,171 +4,47 @@ {{.Title}} - + -
- Production Ready -

{{.Title}}

-

Showcasing production deployment features

+
+
+
+ Production Ready +
+

{{.Title}}

+

Showcasing production deployment features

+
+
-
-

Enabled Features

-
    -
  • Environment-based configuration
  • -
  • Structured JSON logging
  • -
  • Request tracing with trace IDs
  • -
  • Graceful shutdown
  • -
  • Health & readiness checks
  • -
  • Metrics endpoint
  • -
-
+
+ Enabled Features +
    +
  • Environment-based configuration
  • +
  • Structured JSON logging
  • +
  • Request tracing with trace IDs
  • +
  • Graceful shutdown
  • +
  • Health & readiness checks
  • +
  • Metrics endpoint
  • +
+
-
-
{{.Counter}}
-
Counter Value
-
+
+

{{.Counter}}

+

Counter Value

+
-
- - - -
+
+ + + +
- -
+
+ Last updated: {{.LastUpdated}} +
+ + {{if .lvt.DevMode}} diff --git a/profile-progressive/profile.tmpl b/profile-progressive/profile.tmpl index cdd0092..74f244a 100644 --- a/profile-progressive/profile.tmpl +++ b/profile-progressive/profile.tmpl @@ -2,60 +2,55 @@ Progressive Complexity: Profile Demo - + + + -

Edit Profile

-

Zero lvt-* attributes — Tier 1 only

+
+
+

Edit Profile

+

Zero lvt-* attributes — Tier 1 only

+
- {{if .Saved}} -
Profile saved successfully!
- {{end}} - - -
- - - {{if .lvt.HasError "display_name"}} -
{{.lvt.Error "display_name"}}
+ {{if .Saved}} + Profile saved successfully! {{end}} - - - {{if .lvt.HasError "email"}} -
{{.lvt.Error "email"}}
- {{end}} + + - - - {{if .lvt.HasError "bio"}} -
{{.lvt.Error "bio"}}
- {{end}} + + + - -
+ + -
-

Preview

-

Name: {{.DisplayName}}

-

Email: {{.Email}}

-

Bio: {{.Bio}}

-
+
+
Preview
+

Name: {{.DisplayName}}

+

Email: {{.Email}}

+

Bio: {{.Bio}}

+
+
{{if .lvt.DevMode}} diff --git a/profile-progressive/profile_progressive_test.go b/profile-progressive/profile_progressive_test.go index b161c15..fb753cc 100644 --- a/profile-progressive/profile_progressive_test.go +++ b/profile-progressive/profile_progressive_test.go @@ -67,8 +67,8 @@ func TestProfileProgressiveE2E(t *testing.T) { var previewHTML string err := chromedp.Run(ctx, chromedp.OuterHTML("body", &html, chromedp.ByQuery), - chromedp.Evaluate(`document.querySelector('.success') !== null`, &hasSuccess), - chromedp.OuterHTML(".preview", &previewHTML, chromedp.ByQuery), + chromedp.Evaluate(`document.querySelector('ins') !== null`, &hasSuccess), + chromedp.OuterHTML("article", &previewHTML, chromedp.ByQuery), ) if err != nil { t.Fatalf("Failed to get initial state: %v", err) @@ -100,7 +100,7 @@ func TestProfileProgressiveE2E(t *testing.T) { chromedp.Clear(`textarea[name="Bio"]`, chromedp.ByQuery), chromedp.SendKeys(`textarea[name="Bio"]`, "Updated bio text.", chromedp.ByQuery), chromedp.Evaluate(`document.querySelector('button[type="submit"]').click()`, nil), - e2etest.WaitFor(`document.querySelector('.success') !== null`, 5*time.Second), + e2etest.WaitFor(`document.querySelector('ins') !== null`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to save profile: %v", err) @@ -115,7 +115,7 @@ func TestProfileProgressiveE2E(t *testing.T) { } var previewHTML string - if err := chromedp.Run(ctx, chromedp.OuterHTML(".preview", &previewHTML, chromedp.ByQuery)); err != nil { + if err := chromedp.Run(ctx, chromedp.OuterHTML("article", &previewHTML, chromedp.ByQuery)); err != nil { t.Fatalf("Failed to get preview: %v", err) } if !strings.Contains(previewHTML, "John Smith") { @@ -128,15 +128,21 @@ func TestProfileProgressiveE2E(t *testing.T) { t.Run("UpdateProfile", func(t *testing.T) { err := chromedp.Run(ctx, - chromedp.Evaluate(`window.liveTemplateClient.send({action: 'submit', data: {DisplayName: 'Jane Updated', Email: 'jane.updated@example.com', Bio: 'Fixed and saved.'}})`, nil), - e2etest.WaitFor(`document.querySelector('.success') !== null`, 5*time.Second), + chromedp.Clear(`input[name="DisplayName"]`, chromedp.ByQuery), + chromedp.SendKeys(`input[name="DisplayName"]`, "Jane Updated", chromedp.ByQuery), + chromedp.Clear(`input[name="Email"]`, chromedp.ByQuery), + chromedp.SendKeys(`input[name="Email"]`, "jane.updated@example.com", chromedp.ByQuery), + chromedp.Clear(`textarea[name="Bio"]`, chromedp.ByQuery), + chromedp.SendKeys(`textarea[name="Bio"]`, "Fixed and saved.", chromedp.ByQuery), + chromedp.Evaluate(`document.querySelector('button[name="submit"]').click()`, nil), + e2etest.WaitFor(`document.querySelector('ins') !== null`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to update profile: %v", err) } var previewHTML string - if err := chromedp.Run(ctx, chromedp.OuterHTML(".preview", &previewHTML, chromedp.ByQuery)); err != nil { + if err := chromedp.Run(ctx, chromedp.OuterHTML("article", &previewHTML, chromedp.ByQuery)); err != nil { t.Fatalf("Failed to get preview: %v", err) } if !strings.Contains(previewHTML, "Jane Updated") { diff --git a/progressive-enhancement/progressive-enhancement.tmpl b/progressive-enhancement/progressive-enhancement.tmpl index 79921e1..494172a 100644 --- a/progressive-enhancement/progressive-enhancement.tmpl +++ b/progressive-enhancement/progressive-enhancement.tmpl @@ -6,25 +6,16 @@
-

{{.Title}}

-

This app works with or without JavaScript enabled

+
+

{{.Title}}

+

This app works with or without JavaScript enabled

+
diff --git a/progressive-enhancement/progressive_enhancement_test.go b/progressive-enhancement/progressive_enhancement_test.go index 99d08d9..5addb97 100644 --- a/progressive-enhancement/progressive_enhancement_test.go +++ b/progressive-enhancement/progressive_enhancement_test.go @@ -154,7 +154,7 @@ func TestProgressiveEnhancement_JSFormSubmission(t *testing.T) { // Count initial todos var initialTodoCount int err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &initialTodoCount), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &initialTodoCount), ) if err != nil { t.Fatalf("Failed to count initial todos: %v", err) @@ -192,7 +192,7 @@ func TestProgressiveEnhancement_JSFormSubmission(t *testing.T) { // Count todos after submission var finalTodoCount int err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &finalTodoCount), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &finalTodoCount), ) if err != nil { t.Fatalf("Failed to count final todos: %v", err) @@ -524,7 +524,7 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { chromedp.Navigate(server.URL), chromedp.WaitReady(`body`, chromedp.ByQuery), e2etest.WaitFor(`window.liveTemplateClient && window.liveTemplateClient.isReady()`, 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &initialCount), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &initialCount), ) if err != nil { t.Fatalf("Step 1 (navigate) error: %v", err) @@ -537,8 +537,8 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { chromedp.SendKeys(`input[name="title"]`, "E2E Test Todo", chromedp.ByQuery), chromedp.Evaluate(`document.querySelector('button[name="add"]').click()`, nil), // Wait for DOM to update with new item - e2etest.WaitFor(fmt.Sprintf(`document.querySelectorAll('.todo-item').length === %d`, expectedAfterAdd), 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterAddCount), + e2etest.WaitFor(fmt.Sprintf(`document.querySelectorAll('tbody tr').length === %d`, expectedAfterAdd), 5*time.Second), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &afterAddCount), ) if err != nil { t.Fatalf("Step 2 (add) error: %v", err) @@ -552,11 +552,11 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { // Step 3: Toggle the last todo (mark as complete) err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelector('.todo-item:last-child button[name="toggle"]').click()`, nil), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child button[name="toggle"]').click()`, nil), // Wait for the completed class to appear - e2etest.WaitFor(`document.querySelector('.todo-item:last-child').classList.contains('completed')`, 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterToggleCount), - chromedp.Evaluate(`document.querySelector('.todo-item:last-child').classList.contains('completed')`, &hasCompletedClass), + e2etest.WaitFor(`document.querySelector('tbody tr:last-child').querySelector('s') !== null`, 5*time.Second), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &afterToggleCount), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child').querySelector('s') !== null`, &hasCompletedClass), ) if err != nil { t.Fatalf("Step 3 (toggle) error: %v", err) @@ -573,11 +573,11 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { // Step 4: Toggle again (mark as incomplete) err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelector('.todo-item:last-child button[name="toggle"]').click()`, nil), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child button[name="toggle"]').click()`, nil), // Wait for the completed class to be removed - e2etest.WaitFor(`!document.querySelector('.todo-item:last-child').classList.contains('completed')`, 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterUntoggleCount), - chromedp.Evaluate(`document.querySelector('.todo-item:last-child').classList.contains('completed')`, &hasCompletedClassAfterUntoggle), + e2etest.WaitFor(`document.querySelector('tbody tr:last-child').querySelector('s') === null`, 5*time.Second), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &afterUntoggleCount), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child').querySelector('s') !== null`, &hasCompletedClassAfterUntoggle), ) if err != nil { t.Fatalf("Step 4 (untoggle) error: %v", err) @@ -594,10 +594,10 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { // Step 5: Delete the last todo (the one we just added) err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelector('.todo-item:last-child button[name="delete"]').click()`, nil), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child button[name="delete"]').click()`, nil), // Wait for DOM to update with deleted item - e2etest.WaitFor(fmt.Sprintf(`document.querySelectorAll('.todo-item').length === %d`, initialCount), 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterDeleteCount), + e2etest.WaitFor(fmt.Sprintf(`document.querySelectorAll('tbody tr').length === %d`, initialCount), 5*time.Second), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &afterDeleteCount), ) if err != nil { t.Fatalf("Step 5 (delete) error: %v", err) @@ -641,7 +641,7 @@ func TestProgressiveEnhancement_DeleteThenToggle(t *testing.T) { chromedp.Navigate(server.URL), chromedp.WaitReady(`body`, chromedp.ByQuery), e2etest.WaitFor(`window.liveTemplateClient && window.liveTemplateClient.isReady()`, 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &initialCount), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &initialCount), ) if err != nil { t.Fatalf("Step 1 (navigate) error: %v", err) @@ -655,10 +655,10 @@ func TestProgressiveEnhancement_DeleteThenToggle(t *testing.T) { // Step 2: Delete the FIRST todo expectedAfterDelete := initialCount - 1 err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelector('.todo-item:first-child button[name="delete"]').click()`, nil), + chromedp.Evaluate(`document.querySelector('tbody tr:first-child button[name="delete"]').click()`, nil), // Wait for DOM to update with deleted item - e2etest.WaitFor(fmt.Sprintf(`document.querySelectorAll('.todo-item').length === %d`, expectedAfterDelete), 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterDeleteCount), + e2etest.WaitFor(fmt.Sprintf(`document.querySelectorAll('tbody tr').length === %d`, expectedAfterDelete), 5*time.Second), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &afterDeleteCount), ) if err != nil { t.Fatalf("Step 2 (delete) error: %v", err) @@ -674,7 +674,7 @@ func TestProgressiveEnhancement_DeleteThenToggle(t *testing.T) { // Check if it has completed class before toggle var hasCompletedClassBefore bool err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelector('.todo-item:last-child').classList.contains('completed')`, &hasCompletedClassBefore), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child').querySelector('s') !== null`, &hasCompletedClassBefore), ) if err != nil { t.Fatalf("Step 3 (check before toggle) error: %v", err) @@ -682,17 +682,17 @@ func TestProgressiveEnhancement_DeleteThenToggle(t *testing.T) { t.Logf("Last item has completed class before toggle: %v", hasCompletedClassBefore) // Build the wait condition based on whether we expect completed class or not - toggleWaitCondition := `document.querySelector('.todo-item:last-child').classList.contains('completed')` + toggleWaitCondition := `document.querySelector('tbody tr:last-child').querySelector('s') !== null` if hasCompletedClassBefore { - toggleWaitCondition = `!document.querySelector('.todo-item:last-child').classList.contains('completed')` + toggleWaitCondition = `document.querySelector('tbody tr:last-child').querySelector('s') === null` } err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelector('.todo-item:last-child button[name="toggle"]').click()`, nil), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child button[name="toggle"]').click()`, nil), // Wait for the completed class to change e2etest.WaitFor(toggleWaitCondition, 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterToggleCount), - chromedp.Evaluate(`document.querySelector('.todo-item:last-child').classList.contains('completed')`, &hasCompletedClass), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &afterToggleCount), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child').querySelector('s') !== null`, &hasCompletedClass), ) if err != nil { t.Fatalf("Step 3 (toggle) error: %v", err) diff --git a/testing/01_basic/welcome.tmpl b/testing/01_basic/welcome.tmpl index ea2d9c0..ae7f6da 100644 --- a/testing/01_basic/welcome.tmpl +++ b/testing/01_basic/welcome.tmpl @@ -2,11 +2,16 @@ {{.Title}} + + + -

{{.Title}}

-

{{.Message}}

-

Count: {{.Count}}

+
+

{{.Title}}

+

{{.Message}}

+

Count: {{.Count}}

+
{{if .lvt.DevMode}} {{else}} diff --git a/todos-components/todos-components.tmpl b/todos-components/todos-components.tmpl index fcc9967..e9783de 100644 --- a/todos-components/todos-components.tmpl +++ b/todos-components/todos-components.tmpl @@ -10,13 +10,6 @@ {{else}} {{end}} -
@@ -36,34 +29,42 @@ {{/* Todo List */}}
{{if .Todos}} -
+ + {{range .Todos}} -
-
- - - - - -
- - - -
+ + + + + {{end}} - + +
+
+ + + +
+
+ + +
+ + +
+
{{else}}

No todos yet. Add one above!

{{end}} diff --git a/todos-progressive/todos.tmpl b/todos-progressive/todos.tmpl index 93daccf..d151646 100644 --- a/todos-progressive/todos.tmpl +++ b/todos-progressive/todos.tmpl @@ -2,63 +2,60 @@ Progressive Complexity: Todo Demo - + + + -

Todos ({{.ActiveCount}} remaining)

-

Zero lvt-* attributes — standard HTML only

+
+
+

Todos ({{.ActiveCount}} remaining)

+

Zero lvt-* attributes — standard HTML only

+
- -
- - -
- {{if .lvt.HasError "title"}} -
{{.lvt.Error "title"}}
- {{end}} + +
+ + +
+
+ {{if .lvt.HasError "title"}} + {{.lvt.Error "title"}} + {{end}} - -
    - {{range .FilteredItems}} -
  • -
    - - {{.Title}} - - -
    -
  • - {{end}} -
+ + + + {{range .FilteredItems}} + + + + + + {{end}} + +
{{if .Done}}{{.Title}}{{else}}{{.Title}}{{end}} +
+ + +
+
+
+ + +
+
- -
-
-
-
-
+ +
+
+
+
+
+
{{if .lvt.DevMode}} diff --git a/todos-progressive/todos_progressive_test.go b/todos-progressive/todos_progressive_test.go index 03a9471..a1c0eb2 100644 --- a/todos-progressive/todos_progressive_test.go +++ b/todos-progressive/todos_progressive_test.go @@ -67,8 +67,8 @@ func TestTodosProgressiveE2E(t *testing.T) { var hasDone bool err := chromedp.Run(ctx, chromedp.OuterHTML("body", &html, chromedp.ByQuery), - chromedp.Evaluate(`document.querySelectorAll('ul li').length`, &count), - chromedp.Evaluate(`document.querySelector('ul li:nth-child(3) span').classList.contains('done')`, &hasDone), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &count), + chromedp.Evaluate(`document.querySelector('tbody tr:nth-child(3) s') !== null`, &hasDone), ) if err != nil { t.Fatalf("Failed to get initial state: %v", err) @@ -90,14 +90,15 @@ func TestTodosProgressiveE2E(t *testing.T) { t.Errorf("Expected 3 items, got %d", count) } if !hasDone { - t.Error("Third item should have 'done' class") + t.Error("Third item should be struck through") } }) t.Run("AddTodo", func(t *testing.T) { err := chromedp.Run(ctx, - chromedp.Evaluate(`window.liveTemplateClient.send({action: 'submit', data: {Title: 'Buy groceries'}})`, nil), - e2etest.WaitFor(`document.querySelectorAll('ul li').length === 4`, 5*time.Second), + chromedp.SendKeys(`input[name="Title"]`, "Buy groceries", chromedp.ByQuery), + chromedp.Evaluate(`document.querySelector('button[name="submit"]').click()`, nil), + e2etest.WaitFor(`document.querySelectorAll('tbody tr').length === 4`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to add todo: %v", err) @@ -116,15 +117,10 @@ func TestTodosProgressiveE2E(t *testing.T) { }) t.Run("ToggleDone", func(t *testing.T) { - var itemID string - if err := chromedp.Run(ctx, chromedp.Evaluate( - `document.querySelector('ul li:nth-child(1) input[name="id"]').value`, &itemID)); err != nil { - t.Fatalf("Failed to get item ID: %v", err) - } - + // Click the toggle button on the first row err := chromedp.Run(ctx, - chromedp.Evaluate(fmt.Sprintf(`window.liveTemplateClient.send({action: 'toggle', data: {id: '%s'}})`, itemID), nil), - e2etest.WaitFor(`document.querySelector('ul li:nth-child(1) span').classList.contains('done')`, 5*time.Second), + chromedp.Evaluate(`document.querySelector('tbody tr:nth-child(1) button[name="toggle"]').click()`, nil), + e2etest.WaitFor(`document.querySelector('tbody tr:nth-child(1) s') !== null`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to toggle done: %v", err) @@ -140,15 +136,10 @@ func TestTodosProgressiveE2E(t *testing.T) { }) t.Run("ToggleUndo", func(t *testing.T) { - var itemID string - if err := chromedp.Run(ctx, chromedp.Evaluate( - `document.querySelector('ul li:nth-child(1) input[name="id"]').value`, &itemID)); err != nil { - t.Fatalf("Failed to get item ID: %v", err) - } - + // Click the toggle button on the first row again to undo err := chromedp.Run(ctx, - chromedp.Evaluate(fmt.Sprintf(`window.liveTemplateClient.send({action: 'toggle', data: {id: '%s'}})`, itemID), nil), - e2etest.WaitFor(`!document.querySelector('ul li:nth-child(1) span').classList.contains('done')`, 5*time.Second), + chromedp.Evaluate(`document.querySelector('tbody tr:nth-child(1) button[name="toggle"]').click()`, nil), + e2etest.WaitFor(`document.querySelector('tbody tr:nth-child(1) s') === null`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to undo toggle: %v", err) @@ -164,15 +155,10 @@ func TestTodosProgressiveE2E(t *testing.T) { }) t.Run("DeleteTodo", func(t *testing.T) { - var itemID string - if err := chromedp.Run(ctx, chromedp.Evaluate( - `document.querySelector('ul li:last-child input[name="id"]').value`, &itemID)); err != nil { - t.Fatalf("Failed to get item ID: %v", err) - } - + // Click delete on the last row (Buy groceries) err := chromedp.Run(ctx, - chromedp.Evaluate(fmt.Sprintf(`window.liveTemplateClient.send({action: 'delete', data: {id: '%s'}})`, itemID), nil), - e2etest.WaitFor(`document.querySelectorAll('ul li').length === 3`, 5*time.Second), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child button[name="delete"]').click()`, nil), + e2etest.WaitFor(`document.querySelectorAll('tbody tr').length === 3`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to delete todo: %v", err) @@ -188,16 +174,17 @@ func TestTodosProgressiveE2E(t *testing.T) { }) t.Run("FilterActive", func(t *testing.T) { + // Click the "Active" filter button err := chromedp.Run(ctx, - chromedp.Evaluate(`window.liveTemplateClient.send({action: 'filter', data: {filter: 'active'}})`, nil), - e2etest.WaitFor(`document.querySelectorAll('ul li').length === 2`, 5*time.Second), + chromedp.Evaluate(`(() => { const btn = Array.from(document.querySelectorAll('form[name="filter"] button')).find(b => b.textContent.trim() === 'Active'); btn.click(); })()`, nil), + e2etest.WaitFor(`document.querySelectorAll('tbody tr').length === 2`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to filter active: %v", err) } var html string - if err := chromedp.Run(ctx, chromedp.OuterHTML("ul", &html, chromedp.ByQuery)); err != nil { + if err := chromedp.Run(ctx, chromedp.OuterHTML("tbody", &html, chromedp.ByQuery)); err != nil { t.Fatalf("Failed to get HTML: %v", err) } if strings.Contains(html, "Add lvt-* only when needed") { @@ -206,16 +193,17 @@ func TestTodosProgressiveE2E(t *testing.T) { }) t.Run("FilterDone", func(t *testing.T) { + // Click the "Done" filter button err := chromedp.Run(ctx, - chromedp.Evaluate(`window.liveTemplateClient.send({action: 'filter', data: {filter: 'done'}})`, nil), - e2etest.WaitFor(`document.querySelectorAll('ul li').length === 1`, 5*time.Second), + chromedp.Evaluate(`(() => { const btn = Array.from(document.querySelectorAll('form[name="filter"] button')).find(b => b.textContent.trim() === 'Done'); btn.click(); })()`, nil), + e2etest.WaitFor(`document.querySelectorAll('tbody tr').length === 1`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to filter done: %v", err) } var html string - if err := chromedp.Run(ctx, chromedp.OuterHTML("ul", &html, chromedp.ByQuery)); err != nil { + if err := chromedp.Run(ctx, chromedp.OuterHTML("tbody", &html, chromedp.ByQuery)); err != nil { t.Fatalf("Failed to get HTML: %v", err) } if !strings.Contains(html, "Add lvt-* only when needed") { @@ -224,9 +212,10 @@ func TestTodosProgressiveE2E(t *testing.T) { }) t.Run("FilterAll", func(t *testing.T) { + // Click the "All" filter button err := chromedp.Run(ctx, - chromedp.Evaluate(`window.liveTemplateClient.send({action: 'filter', data: {filter: 'all'}})`, nil), - e2etest.WaitFor(`document.querySelectorAll('ul li').length === 3`, 5*time.Second), + chromedp.Evaluate(`(() => { const btn = Array.from(document.querySelectorAll('form[name="filter"] button')).find(b => b.textContent.trim() === 'All'); btn.click(); })()`, nil), + e2etest.WaitFor(`document.querySelectorAll('tbody tr').length === 3`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to filter all: %v", err) diff --git a/ws-disabled/ws-disabled.tmpl b/ws-disabled/ws-disabled.tmpl index e1af344..d9870ca 100644 --- a/ws-disabled/ws-disabled.tmpl +++ b/ws-disabled/ws-disabled.tmpl @@ -13,10 +13,10 @@ and applies tree-based DOM updates without page reloads.

{{if .lvt.Flash "success"}} - {{.lvt.Flash "success"}} + {{.lvt.Flash "success"}} {{end}} {{if .lvt.Flash "error"}} - {{.lvt.Flash "error"}} + {{.lvt.Flash "error"}} {{end}}