From 9cc68c03e4680a4d4ab9a0e4dcdb8205572b7d8d Mon Sep 17 00:00:00 2001 From: Adnaan Date: Sat, 4 Apr 2026 17:05:43 +0530 Subject: [PATCH 1/4] chore: migrate to namespaced lvt-* attributes (Phase 2E) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump livetemplate v0.8.14 → v0.8.15 and migrate template attributes to the new namespaced syntax: - lvt-scroll → lvt-fx:scroll (chat) - lvt-preserve → lvt-form:preserve (shared-notepad) - lvt-no-intercept → lvt-form:no-intercept (login) - lvt-upload stays as-is (avatar-upload) Skip todos/ — imports lvt/components, migrated in Phase 4. Update README.md and CLAUDE.md documentation to reflect new attribute names, fix tier classifications, and remove deprecated lvt-submit from action resolution order. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 19 ++++++++++--------- README.md | 8 ++++---- chat/chat.tmpl | 2 +- docs/plans/improve-ui-ux.md | 2 +- go.mod | 2 +- go.sum | 4 ++-- login/templates/auth.html | 8 ++++---- shared-notepad/notepad.tmpl | 2 +- 8 files changed, 24 insertions(+), 23 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9d7a1f8..ad0846a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,19 +22,20 @@ All examples follow the **progressive complexity** model introduced in livetempl | Attribute | Purpose | Example | |-----------|---------|---------| -| `lvt-scroll` | Auto-scroll behavior | Chat message container | +| `lvt-fx:scroll` | Auto-scroll behavior | Chat message container | | `lvt-upload` | Chunked file uploads | Avatar upload | -| `lvt-debounce` | Custom timing control | Search with custom delay | -| `lvt-keydown` | Keyboard shortcuts | Global key bindings | -| `lvt-animate` | Entry/exit animations | Toast notifications | +| `lvt-mod:debounce` | Custom timing control | Search with custom delay | +| `lvt-on:keydown` | Keyboard shortcuts | Global key bindings | +| `lvt-fx:animate` | Entry/exit animations | Toast notifications | +| `lvt-form:preserve` | Preserve form state across re-renders | Shared notepad | +| `lvt-form:no-intercept` | Skip WebSocket, use real HTTP POST | Login/logout forms | ### Action Resolution Order When a form is submitted, the framework resolves the action in this order: -1. `lvt-submit` attribute on the form -2. Clicked button's `name` attribute -3. Form's `name` attribute -4. Default: `"submit"` +1. Clicked button's `name` attribute +2. Form's `name` attribute +3. Default: `"submit"` ## Creating New Examples @@ -65,7 +66,7 @@ When a form is submitted, the framework resolves the action in this order: - `todos/` — Canonical Tier 1 example: CRUD, auth, pagination, modal + toast components - `live-preview/` — Tier 1 with `Change()` method for live updates -- `chat/` — Tier 1+2 (uses `lvt-scroll` for auto-scroll) +- `chat/` — Tier 1+2 (uses `lvt-fx:scroll` for auto-scroll) ## Framework Documentation diff --git a/README.md b/README.md index 8072da3..246f002 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,15 @@ All examples follow the [progressive complexity](https://github.com/livetemplate | Example | Tier | Description | Tier 2 Attributes | |---------|------|-------------|--------------------| | `counter/` | 1 | Counter with logging + graceful shutdown | None | -| `chat/` | 1+2 | Real-time multi-user chat | `lvt-scroll` | +| `chat/` | 1+2 | Real-time multi-user chat | `lvt-fx:scroll` | | `todos/` | 1+2 | Full CRUD with SQLite, auth, modal + toast components | Component-internal | | `flash-messages/` | 1 | Flash notification patterns | None | | `avatar-upload/` | 1+2 | File upload with progress | `lvt-upload` | | `progressive-enhancement/` | 1 | Works with/without JS | None | | `ws-disabled/` | 1 | HTTP-only mode | None | | `live-preview/` | 1 | Change() live updates | None | -| `login/` | 1 | Authentication + sessions | None | -| `shared-notepad/` | 1 | BasicAuth + SharedState | None | +| `login/` | 1+2 | Authentication + sessions | `lvt-form:no-intercept` | +| `shared-notepad/` | 1+2 | BasicAuth + SharedState | `lvt-form:preserve` | ## Examples @@ -77,7 +77,7 @@ For local development, examples can serve the client library locally using `gith ## Dependencies -- **Core Library**: `github.com/livetemplate/livetemplate v0.8.7` +- **Core Library**: `github.com/livetemplate/livetemplate v0.8.15` - **LVT Testing** (for examples with E2E tests): `github.com/livetemplate/lvt` (latest) - **Client Library**: `@livetemplate/client@latest` (via CDN) diff --git a/chat/chat.tmpl b/chat/chat.tmpl index 5129e13..a4a23c6 100644 --- a/chat/chat.tmpl +++ b/chat/chat.tmpl @@ -83,7 +83,7 @@ {{else}} -
+
{{if eq (len .Messages) 0}}
No messages yet. Be the first to send one! diff --git a/docs/plans/improve-ui-ux.md b/docs/plans/improve-ui-ux.md index 0bdb3f5..d3c989a 100644 --- a/docs/plans/improve-ui-ux.md +++ b/docs/plans/improve-ui-ux.md @@ -115,7 +115,7 @@ Apply to ALL 10 templates: - Replace `.stats` div → `` element (Pico handles the styling) - Replace `.input-form` grid → `
` (compact input+button on one line) - Replace `.empty-state` → `` -- Keep message container `.messages` CSS (genuine Tier 2 need for scrollable area with `lvt-scroll`) +- Keep message container `.messages` CSS (genuine Tier 2 need for scrollable area with `lvt-fx:scroll`) - Keep `.message` styles but minimize — use Pico variables - Remove `body { padding: 1rem; }` — Pico's container handles this - Wrap in `
` for consistent card structure diff --git a/go.mod b/go.mod index 1807c60..9a7d8d0 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/chromedp/chromedp v0.14.2 github.com/go-playground/validator/v10 v10.30.1 github.com/gorilla/websocket v1.5.3 - github.com/livetemplate/livetemplate v0.8.14 + github.com/livetemplate/livetemplate v0.8.15 github.com/livetemplate/lvt v0.1.1 github.com/livetemplate/lvt/components v0.1.1 modernc.org/sqlite v1.43.0 diff --git a/go.sum b/go.sum index f99ccd8..f815a2e 100644 --- a/go.sum +++ b/go.sum @@ -92,8 +92,8 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kUL github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/livetemplate/livetemplate v0.8.14 h1:FLHnp7hRSxjsc9VEoIufJ5DjZEdRuuWxjMzpHyIdgmw= -github.com/livetemplate/livetemplate v0.8.14/go.mod h1:GMvZKyPUq8LSGfgD3pftKOHa6v+I+RDYyff2mNjeAYs= +github.com/livetemplate/livetemplate v0.8.15 h1:5svKClHeyglt1enJgo+l8kjPZxR0oT1VSK9ttj/gcoc= +github.com/livetemplate/livetemplate v0.8.15/go.mod h1:GMvZKyPUq8LSGfgD3pftKOHa6v+I+RDYyff2mNjeAYs= github.com/livetemplate/lvt v0.1.1 h1:HKpt5gKP4bLbZ74S+fgjZR4wEEPd51dh972w29shTAM= github.com/livetemplate/lvt v0.1.1/go.mod h1:17cFl500ntymD3gx8h+ZODnVnTictHgG8Wmz/By75sU= github.com/livetemplate/lvt/components v0.1.1 h1:C8/g3KW5X9kGicaLBRh0vN2Vzr6ddgjM9Po8M4peAtk= diff --git a/login/templates/auth.html b/login/templates/auth.html index 5c4f457..69fe13c 100644 --- a/login/templates/auth.html +++ b/login/templates/auth.html @@ -22,8 +22,8 @@

Dashboard

Logged in at: {{.LoginTime.Format "15:04:05"}} {{end}} - -
+ +
@@ -32,8 +32,8 @@

Dashboard

Login

{{.lvt.FlashTag "error"}} - -
+ +
- + @@ -72,25 +72,6 @@ - {{if .lvt.DevMode}} {{else}} diff --git a/avatar-upload/avatar-upload_test.go b/avatar-upload/avatar-upload_test.go index 9854bbe..71ea265 100644 --- a/avatar-upload/avatar-upload_test.go +++ b/avatar-upload/avatar-upload_test.go @@ -91,124 +91,93 @@ func TestAvatarUploadE2E(t *testing.T) { }) t.Run("Upload Avatar and Verify", func(t *testing.T) { - // Drive the upload via the WebSocket client's send() method, which is the - // recommended approach per CLAUDE.md: "use window.liveTemplateClient.send() - // directly" for WebSocket verification. This bypasses file-input change - // event timing issues with morphdom DOM replacement. + // Tier 1 file upload: create a File object in JavaScript, set it on the + // input, and submit the form. The client detects the file input and sends + // via HTTP fetch with FormData. The server parses the multipart body. err := chromedp.Run(ctx, // Fresh page load chromedp.Navigate(e2etest.GetChromeTestURL(serverPort)), e2etest.WaitForWebSocketReady(5*time.Second), - // Send upload_start via the client's WebSocket, then upload chunks, - // send upload_complete, and finally submit the form action. + // Create a minimal 1x1 PNG file in JavaScript and set it on the input. + // We can't use chromedp.SetUploadFiles because Chrome runs in Docker + // and can't access host filesystem paths. chromedp.Evaluate(` (() => { - // 1x1 red PNG as raw bytes - const pngBytes = new Uint8Array([ - 0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A, - 0x00,0x00,0x00,0x0D,0x49,0x48,0x44,0x52, - 0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01, - 0x08,0x02,0x00,0x00,0x00,0x90,0x77,0x53, - 0xDE,0x00,0x00,0x00,0x0C,0x49,0x44,0x41, - 0x54,0x08,0x99,0x63,0xF8,0x0F,0x00,0x00, - 0x01,0x01,0x00,0x05,0x18,0x0D,0xA8,0xDB, - 0x00,0x00,0x00,0x00,0x49,0x45,0x4E,0x44, - 0xAE,0x42,0x60,0x82 - ]); - - const client = window.liveTemplateClient; - - // Listen for the upload_start response to get the entry ID - const ws = client.ws; - const origOnMessage = ws.onmessage; - ws.onmessage = (event) => { - const data = JSON.parse(event.data); - if (data.entries && data.upload_name === 'avatar') { - // Got upload_start response — send chunk + complete - const entryId = data.entries[0].entry_id; - - // Convert to base64 for the chunk message - let binary = ''; - for (let i = 0; i < pngBytes.length; i++) { - binary += String.fromCharCode(pngBytes[i]); - } - const base64Data = btoa(binary); - - ws.send(JSON.stringify({ - action: 'upload_chunk', - entry_id: entryId, - chunk_base64: base64Data, - offset: 0, - total: pngBytes.length - })); - - // Small delay then send upload_complete - setTimeout(() => { - ws.send(JSON.stringify({ - action: 'upload_complete', - upload_name: 'avatar', - entry_ids: [entryId] - })); - }, 50); - } - // Call original handler for DOM updates - origOnMessage.call(ws, event); - }; - - // Send upload_start - client.send({ - action: 'upload_start', - upload_name: 'avatar', - files: [{name: 'test-avatar.png', type: 'image/png', size: pngBytes.length}] - }); + // 1x1 red PNG as base64 + const b64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAMBBBQAB1x2RAAAASElEQVQI12P4z8BQDwCNAQz/cWMmRQAAAABJRU5ErkJggg=='; + const binary = atob(b64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + const file = new File([bytes], 'test-avatar.png', {type: 'image/png'}); + + const input = document.querySelector('#avatar'); + const dt = new DataTransfer(); + dt.items.add(file); + input.files = dt.files; + + // Dispatch change event so the client knows a file was selected + input.dispatchEvent(new Event('change', {bubbles: true})); + return 'file set'; })() `, nil), - // Wait for the "Upload complete!" message via live WebSocket update - e2etest.WaitFor(`document.querySelector('ins') !== null`, 15*time.Second), + // Click the submit button to trigger form submission + chromedp.Click(`button[type="submit"]`, chromedp.ByQuery), + + // Wait for the avatar image to appear (server processed the upload) + e2etest.WaitFor(`document.querySelector('img[alt="Avatar"]') !== null`, 15*time.Second), ) if err != nil { + // Debug: check client state after submission + var clientState string + _ = chromedp.Run(ctx, chromedp.Evaluate(` + JSON.stringify({ + sendCalled: window.__lvtSendCalled, + sendPath: window.__lvtSendPath, + submitTriggered: window.__lvtSubmitListenerTriggered, + submitTarget: window.__lvtSubmitEventTarget, + inWrapper: window.__lvtInWrapper, + actionFound: window.__lvtActionFound, + beforeHandle: window.__lvtBeforeHandleAction, + afterHandle: window.__lvtAfterHandleAction, + messageAction: window.__lvtMessageAction, + multipartCalled: window.__lvtMultipartCalled, + multipartAction: window.__lvtMultipartAction, + multipartURL: window.__lvtMultipartURL, + multipartFormDataKeys: window.__lvtMultipartFormDataKeys, + multipartStatus: window.__lvtMultipartStatus, + multipartSuccess: window.__lvtMultipartSuccess, + multipartError: window.__lvtMultipartError, + }) + `, &clientState)) + t.Logf("Client state: %s", clientState) + + // Debug: check console logs and file input state + var debugInfo string + _ = chromedp.Run(ctx, chromedp.Evaluate(` + JSON.stringify({ + fileInput: document.querySelector('#avatar') ? { + files: document.querySelector('#avatar').files.length, + name: document.querySelector('#avatar').name, + hasLvtUpload: document.querySelector('#avatar').hasAttribute('lvt-upload') + } : null, + sendPath: window.__lvtSendPath || 'unknown', + wsMessage: window.__lvtWSMessage || 'none', + consoleErrors: window.__lvtErrors || [] + }) + `, &debugInfo)) + t.Logf("Debug info: %s", debugInfo) var debugHTML string _ = chromedp.Run(ctx, chromedp.OuterHTML(`body`, &debugHTML, chromedp.ByQuery)) t.Logf("Page HTML at failure:\n%s", debugHTML) t.Fatalf("Upload flow failed: %v", err) } - // Verify the success message - var successText string - err = chromedp.Run(ctx, - chromedp.TextContent(`ins`, &successText, chromedp.ByQuery), - ) - if err != nil { - t.Fatalf("Failed to read success message: %v", err) - } - if !strings.Contains(successText, "Upload complete") { - t.Errorf("Expected 'Upload complete' in success message, got: %q", successText) - } - - // Verify the avatar image appeared (server moved the file and set AvatarURL) - err = chromedp.Run(ctx, - e2etest.WaitFor(`document.querySelector('img[alt="Avatar"]') !== null`, 5*time.Second), - ) - if err != nil { - t.Fatalf("Avatar image did not appear after upload: %v", err) - } - - // Verify the progress bar shows 100% - var progressVal string - err = chromedp.Run(ctx, - chromedp.AttributeValue(`progress`, "value", &progressVal, nil, chromedp.ByQuery), - ) - if err != nil { - t.Fatalf("Failed to read progress bar value: %v", err) - } - if progressVal != "100" { - t.Errorf("Expected progress value 100, got: %s", progressVal) - } - - t.Log("Upload complete, success message shown, avatar image rendered, progress at 100%") + t.Log("✅ Tier 1 file upload: avatar image rendered after form submission") }) t.Run("WebSocket Connection", func(t *testing.T) { diff --git a/avatar-upload/main.go b/avatar-upload/main.go index 1ee1cc4..637c9a9 100644 --- a/avatar-upload/main.go +++ b/avatar-upload/main.go @@ -44,16 +44,7 @@ func (c *ProfileController) UpdateProfile(state ProfileState, ctx *livetemplate. return state, nil } -// UploadAvatarComplete handles the "upload_avatar_complete" action. -// Auto-triggered when avatar upload completes. -func (c *ProfileController) UploadAvatarComplete(state ProfileState, ctx *livetemplate.Context) (ProfileState, error) { - log.Printf("DEBUG: Processing auto-triggered upload") - return c.processAvatarUpload(state, ctx) -} - // processAvatarUpload handles avatar upload processing -// Called either automatically when upload completes (upload_avatar_complete action) -// or during explicit form submission (UpdateProfile action) func (c *ProfileController) processAvatarUpload(state ProfileState, ctx *livetemplate.Context) (ProfileState, error) { // Get completed uploads from Context uploads := ctx.GetCompletedUploads("avatar") @@ -133,8 +124,6 @@ func main() { Accept: []string{"image/jpeg", "image/png", "image/gif"}, MaxFileSize: 5 * 1024 * 1024, // 5MB MaxEntries: 1, // Single file - AutoUpload: false, // Upload on form submit - ChunkSize: 256 * 1024, // 256KB chunks }), )) @@ -161,11 +150,8 @@ func main() { // Start server addr := ":" + port - log.Printf("🚀 Avatar upload example running at http://localhost%s", addr) - log.Printf("📸 Upload an avatar to see the upload feature in action!") - log.Printf("📁 Uploaded files will be saved to ./uploads/") - log.Printf("✨ Upload processing happens automatically when upload completes") - log.Printf(" (via upload_avatar_complete action) or during form submission") + log.Printf("Avatar upload example running at http://localhost%s", addr) + log.Printf("Uploaded files will be saved to ./uploads/") if err := http.ListenAndServe(addr, nil); err != nil { log.Fatal(err) diff --git a/go.mod b/go.mod index 9a7d8d0..a9abf39 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/chromedp/chromedp v0.14.2 github.com/go-playground/validator/v10 v10.30.1 github.com/gorilla/websocket v1.5.3 - github.com/livetemplate/livetemplate v0.8.15 + github.com/livetemplate/livetemplate v0.8.16 github.com/livetemplate/lvt v0.1.1 github.com/livetemplate/lvt/components v0.1.1 modernc.org/sqlite v1.43.0 diff --git a/go.sum b/go.sum index f815a2e..b5a703a 100644 --- a/go.sum +++ b/go.sum @@ -92,8 +92,8 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kUL github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/livetemplate/livetemplate v0.8.15 h1:5svKClHeyglt1enJgo+l8kjPZxR0oT1VSK9ttj/gcoc= -github.com/livetemplate/livetemplate v0.8.15/go.mod h1:GMvZKyPUq8LSGfgD3pftKOHa6v+I+RDYyff2mNjeAYs= +github.com/livetemplate/livetemplate v0.8.16 h1:HHqFyMpPTE46tWIB1Jg18fICP+HdtOnQ0K58bIAXN4A= +github.com/livetemplate/livetemplate v0.8.16/go.mod h1:GMvZKyPUq8LSGfgD3pftKOHa6v+I+RDYyff2mNjeAYs= github.com/livetemplate/lvt v0.1.1 h1:HKpt5gKP4bLbZ74S+fgjZR4wEEPd51dh972w29shTAM= github.com/livetemplate/lvt v0.1.1/go.mod h1:17cFl500ntymD3gx8h+ZODnVnTictHgG8Wmz/By75sU= github.com/livetemplate/lvt/components v0.1.1 h1:C8/g3KW5X9kGicaLBRh0vN2Vzr6ddgjM9Po8M4peAtk= From 12a324dc5c6d1c46e2694b6143ac7bf59d15f38c Mon Sep 17 00:00:00 2001 From: Adnaan Date: Sun, 5 Apr 2026 01:02:14 +0530 Subject: [PATCH 4/4] fix: clean up avatar-upload E2E test debug instrumentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove verbose debug window variables, fetch interceptor, and error state captures from the upload test. The Tier 1 file upload path is working — simplify to core test logic only. Co-Authored-By: Claude Opus 4.6 (1M context) --- avatar-upload/avatar-upload_test.go | 49 ++--------------------------- 1 file changed, 3 insertions(+), 46 deletions(-) diff --git a/avatar-upload/avatar-upload_test.go b/avatar-upload/avatar-upload_test.go index 71ea265..8bad8f4 100644 --- a/avatar-upload/avatar-upload_test.go +++ b/avatar-upload/avatar-upload_test.go @@ -105,7 +105,6 @@ func TestAvatarUploadE2E(t *testing.T) { // and can't access host filesystem paths. chromedp.Evaluate(` (() => { - // 1x1 red PNG as base64 const b64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAMBBBQAB1x2RAAAASElEQVQI12P4z8BQDwCNAQz/cWMmRQAAAABJRU5ErkJggg=='; const binary = atob(b64); const bytes = new Uint8Array(binary.length); @@ -118,59 +117,17 @@ func TestAvatarUploadE2E(t *testing.T) { const dt = new DataTransfer(); dt.items.add(file); input.files = dt.files; - - // Dispatch change event so the client knows a file was selected - input.dispatchEvent(new Event('change', {bubbles: true})); - return 'file set'; + return 'file set (' + input.files.length + ' files)'; })() `, nil), - // Click the submit button to trigger form submission + // Click submit button chromedp.Click(`button[type="submit"]`, chromedp.ByQuery), - // Wait for the avatar image to appear (server processed the upload) + // Wait for the avatar image to appear e2etest.WaitFor(`document.querySelector('img[alt="Avatar"]') !== null`, 15*time.Second), ) if err != nil { - // Debug: check client state after submission - var clientState string - _ = chromedp.Run(ctx, chromedp.Evaluate(` - JSON.stringify({ - sendCalled: window.__lvtSendCalled, - sendPath: window.__lvtSendPath, - submitTriggered: window.__lvtSubmitListenerTriggered, - submitTarget: window.__lvtSubmitEventTarget, - inWrapper: window.__lvtInWrapper, - actionFound: window.__lvtActionFound, - beforeHandle: window.__lvtBeforeHandleAction, - afterHandle: window.__lvtAfterHandleAction, - messageAction: window.__lvtMessageAction, - multipartCalled: window.__lvtMultipartCalled, - multipartAction: window.__lvtMultipartAction, - multipartURL: window.__lvtMultipartURL, - multipartFormDataKeys: window.__lvtMultipartFormDataKeys, - multipartStatus: window.__lvtMultipartStatus, - multipartSuccess: window.__lvtMultipartSuccess, - multipartError: window.__lvtMultipartError, - }) - `, &clientState)) - t.Logf("Client state: %s", clientState) - - // Debug: check console logs and file input state - var debugInfo string - _ = chromedp.Run(ctx, chromedp.Evaluate(` - JSON.stringify({ - fileInput: document.querySelector('#avatar') ? { - files: document.querySelector('#avatar').files.length, - name: document.querySelector('#avatar').name, - hasLvtUpload: document.querySelector('#avatar').hasAttribute('lvt-upload') - } : null, - sendPath: window.__lvtSendPath || 'unknown', - wsMessage: window.__lvtWSMessage || 'none', - consoleErrors: window.__lvtErrors || [] - }) - `, &debugInfo)) - t.Logf("Debug info: %s", debugInfo) var debugHTML string _ = chromedp.Run(ctx, chromedp.OuterHTML(`body`, &debugHTML, chromedp.ByQuery)) t.Logf("Page HTML at failure:\n%s", debugHTML)