From a7e4d0fdfa028da5e802cb2b49799c81f0c64684 Mon Sep 17 00:00:00 2001 From: Adnaan Date: Mon, 20 Apr 2026 03:41:47 +0200 Subject: [PATCH 1/6] refactor: replace id="scroll-sentinel" with lvt-scroll-sentinel attribute Updates all generator templates, kit components, golden files, and E2E tests to use the new lvt-scroll-sentinel boolean attribute. Aligns with the framework's lvt-* attribute convention. Co-Authored-By: Claude Opus 4.6 --- e2e/complete_workflow_test.go | 4 ++-- e2e/rendering_test.go | 6 +++--- e2e/resource_generation_test.go | 4 ++-- e2e/testdata/golden/resource_template.tmpl.golden | 2 +- e2e/tutorial_test.go | 6 +++--- internal/generator/templates/components/pagination.tmpl | 2 +- internal/generator/templates/resource/template.tmpl.tmpl | 4 ++-- internal/kits/system/multi/components/pagination.tmpl | 2 +- .../kits/system/multi/templates/resource/template.tmpl.tmpl | 4 ++-- internal/kits/system/single/components/pagination.tmpl | 2 +- .../system/single/templates/resource/template.tmpl.tmpl | 4 ++-- testdata/golden/resource_template.tmpl.golden | 2 +- 12 files changed, 21 insertions(+), 21 deletions(-) diff --git a/e2e/complete_workflow_test.go b/e2e/complete_workflow_test.go index f06276fa..478fab58 100644 --- a/e2e/complete_workflow_test.go +++ b/e2e/complete_workflow_test.go @@ -908,8 +908,8 @@ func TestCompleteWorkflow_BlogApp(t *testing.T) { t.Fatalf("Failed to read template: %v", err) } - if !strings.Contains(string(tmplContent), `id="scroll-sentinel"`) { - t.Error("❌ Template missing scroll-sentinel") + if !strings.Contains(string(tmplContent), `lvt-scroll-sentinel`) { + t.Error("❌ Template missing lvt-scroll-sentinel") } else { t.Log("✅ Scroll sentinel element present") } diff --git a/e2e/rendering_test.go b/e2e/rendering_test.go index a87b8c80..5f20fb06 100644 --- a/e2e/rendering_test.go +++ b/e2e/rendering_test.go @@ -1288,7 +1288,7 @@ func TestRendering_InfiniteScroll(t *testing.T) { @@ -1301,7 +1301,7 @@ func TestRendering_InfiniteScroll(t *testing.T) {
Item 3
Item 4
Item 5
-
+
Loading more...
@@ -1310,7 +1310,7 @@ func TestRendering_InfiniteScroll(t *testing.T) { let isLoading = false; const container = document.getElementById('scroll-container'); - const sentinel = document.getElementById('scroll-sentinel'); + const sentinel = document.querySelector('[lvt-scroll-sentinel]'); const loading = document.getElementById('loading'); // Intersection Observer for infinite scroll diff --git a/e2e/resource_generation_test.go b/e2e/resource_generation_test.go index 288b195b..b9d28af8 100644 --- a/e2e/resource_generation_test.go +++ b/e2e/resource_generation_test.go @@ -187,8 +187,8 @@ func TestResourceGen_PaginationInfinite(t *testing.T) { t.Fatalf("Failed to read template: %v", err) } - if !strings.Contains(string(tmpl), `id="scroll-sentinel"`) { - t.Error("Template missing scroll-sentinel element") + if !strings.Contains(string(tmpl), `lvt-scroll-sentinel`) { + t.Error("Template missing lvt-scroll-sentinel element") } // Validate generated code compiles diff --git a/e2e/testdata/golden/resource_template.tmpl.golden b/e2e/testdata/golden/resource_template.tmpl.golden index 29c8dcbc..9d7321b2 100644 --- a/e2e/testdata/golden/resource_template.tmpl.golden +++ b/e2e/testdata/golden/resource_template.tmpl.golden @@ -198,7 +198,7 @@ Loading more... {{end}} -
+
{{end}} {{end}} diff --git a/e2e/tutorial_test.go b/e2e/tutorial_test.go index 6e9707d5..05a6b37d 100644 --- a/e2e/tutorial_test.go +++ b/e2e/tutorial_test.go @@ -828,10 +828,10 @@ func TestTutorialE2E(t *testing.T) { } tmplStr := string(tmplContent) - if !strings.Contains(tmplStr, `id="scroll-sentinel"`) { - t.Error("❌ Template does not contain scroll-sentinel element") + if !strings.Contains(tmplStr, `lvt-scroll-sentinel`) { + t.Error("❌ Template does not contain lvt-scroll-sentinel element") } else { - t.Log("✅ Template contains scroll-sentinel element for infinite scroll") + t.Log("✅ Template contains lvt-scroll-sentinel element for infinite scroll") } // Verify the sentinel appears in actual rendered HTML when there are no template errors diff --git a/internal/generator/templates/components/pagination.tmpl b/internal/generator/templates/components/pagination.tmpl index 5ae09c94..20469b36 100644 --- a/internal/generator/templates/components/pagination.tmpl +++ b/internal/generator/templates/components/pagination.tmpl @@ -19,7 +19,7 @@ Loading more... {{end}} -
+
{{end}} {{end}} diff --git a/internal/generator/templates/resource/template.tmpl.tmpl b/internal/generator/templates/resource/template.tmpl.tmpl index 04ef3a25..855fc3bf 100644 --- a/internal/generator/templates/resource/template.tmpl.tmpl +++ b/internal/generator/templates/resource/template.tmpl.tmpl @@ -230,7 +230,7 @@ Loading more... {{end}} -
+
{{end}} [[- else if eq .PaginationMode "load-more"]] {{if .HasMore}} @@ -342,7 +342,7 @@ } function setupInfiniteScroll() { - const sentinel = document.getElementById('scroll-sentinel'); + const sentinel = document.querySelector('[lvt-scroll-sentinel]'); if (!sentinel) return; const observer = new IntersectionObserver(function(entries) { diff --git a/internal/kits/system/multi/components/pagination.tmpl b/internal/kits/system/multi/components/pagination.tmpl index 96c7026a..b9985b51 100644 --- a/internal/kits/system/multi/components/pagination.tmpl +++ b/internal/kits/system/multi/components/pagination.tmpl @@ -19,7 +19,7 @@ Loading more... {{end}} -
+
{{end}} {{end}} diff --git a/internal/kits/system/multi/templates/resource/template.tmpl.tmpl b/internal/kits/system/multi/templates/resource/template.tmpl.tmpl index 30f1b26d..5f5c4bf1 100644 --- a/internal/kits/system/multi/templates/resource/template.tmpl.tmpl +++ b/internal/kits/system/multi/templates/resource/template.tmpl.tmpl @@ -241,7 +241,7 @@ Loading more... {{end}} -
+
{{end}} [[- else if eq .PaginationMode "load-more"]] {{if .HasMore}} @@ -402,7 +402,7 @@ } function setupInfiniteScroll() { - const sentinel = document.getElementById('scroll-sentinel'); + const sentinel = document.querySelector('[lvt-scroll-sentinel]'); if (!sentinel) return; const observer = new IntersectionObserver(function(entries) { diff --git a/internal/kits/system/single/components/pagination.tmpl b/internal/kits/system/single/components/pagination.tmpl index 96c7026a..b9985b51 100644 --- a/internal/kits/system/single/components/pagination.tmpl +++ b/internal/kits/system/single/components/pagination.tmpl @@ -19,7 +19,7 @@ Loading more... {{end}} -
+
{{end}} {{end}} diff --git a/internal/kits/system/single/templates/resource/template.tmpl.tmpl b/internal/kits/system/single/templates/resource/template.tmpl.tmpl index 30f1b26d..5f5c4bf1 100644 --- a/internal/kits/system/single/templates/resource/template.tmpl.tmpl +++ b/internal/kits/system/single/templates/resource/template.tmpl.tmpl @@ -241,7 +241,7 @@ Loading more... {{end}} -
+
{{end}} [[- else if eq .PaginationMode "load-more"]] {{if .HasMore}} @@ -402,7 +402,7 @@ } function setupInfiniteScroll() { - const sentinel = document.getElementById('scroll-sentinel'); + const sentinel = document.querySelector('[lvt-scroll-sentinel]'); if (!sentinel) return; const observer = new IntersectionObserver(function(entries) { diff --git a/testdata/golden/resource_template.tmpl.golden b/testdata/golden/resource_template.tmpl.golden index b25026c5..a49827f5 100644 --- a/testdata/golden/resource_template.tmpl.golden +++ b/testdata/golden/resource_template.tmpl.golden @@ -252,7 +252,7 @@ Loading more... {{end}} -
+
{{end}} {{end}} From 499849287a53db246dd79c1bedace5833a32832d Mon Sep 17 00:00:00 2001 From: Adnaan Date: Mon, 20 Apr 2026 03:49:43 +0200 Subject: [PATCH 2/6] fix: tighten sentinel assertions to match element markup Checks for `
--- e2e/complete_workflow_test.go | 2 +- e2e/resource_generation_test.go | 2 +- e2e/tutorial_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/complete_workflow_test.go b/e2e/complete_workflow_test.go index 478fab58..6591f4c3 100644 --- a/e2e/complete_workflow_test.go +++ b/e2e/complete_workflow_test.go @@ -908,7 +908,7 @@ func TestCompleteWorkflow_BlogApp(t *testing.T) { t.Fatalf("Failed to read template: %v", err) } - if !strings.Contains(string(tmplContent), `lvt-scroll-sentinel`) { + if !strings.Contains(string(tmplContent), `
Date: Mon, 20 Apr 2026 05:18:08 +0200 Subject: [PATCH 3/6] fix: use real click instead of dispatchEvent for validation test dispatchEvent(new Event('submit')) creates a synthetic event that the client intercepts but WebSocket.send() data never reaches the server. Use chromedp.Click on the submit button (matching the working Add Post test) with noValidate=true to bypass HTML5 validation. Also removes all debugging instrumentation added during investigation. Co-Authored-By: Claude Opus 4.6 --- e2e/complete_workflow_test.go | 61 +++++------------------------------ e2e/tutorial_test.go | 59 ++++++--------------------------- 2 files changed, 17 insertions(+), 103 deletions(-) diff --git a/e2e/complete_workflow_test.go b/e2e/complete_workflow_test.go index 6591f4c3..76608f9d 100644 --- a/e2e/complete_workflow_test.go +++ b/e2e/complete_workflow_test.go @@ -779,13 +779,9 @@ func TestCompleteWorkflow_BlogApp(t *testing.T) { }) // Test 11.5: Validation Errors - // Bug was fixed on 2025-10-24 - see BUG-VALIDATION-CONDITIONALS.md:409 t.Run("Validation Errors", func(t *testing.T) { ctx, cancel := createBrowserContext() defer cancel() - // Use 180s timeout - validation test does multiple operations and can be slow - // Running against Docker container adds significant overhead compared to local server. - // Increased from 120s to 180s to handle Docker networking and resource contention. ctx, timeoutCancel := context.WithTimeout(ctx, 180*time.Second) defer timeoutCancel() @@ -796,7 +792,7 @@ func TestCompleteWorkflow_BlogApp(t *testing.T) { waitForWebSocketReady(5*time.Second), chromedp.WaitVisible(`[data-lvt-id]`, chromedp.ByQuery), - // Open add modal via DOM manipulation (more reliable than click event delegation) + // Open add modal chromedp.WaitVisible(`[command="show-modal"][commandfor="add-modal"]`, chromedp.ByQuery), chromedp.Evaluate(` (() => { @@ -806,28 +802,18 @@ func TestCompleteWorkflow_BlogApp(t *testing.T) { } })() `, nil), - // Wait for form to be visible (modal is open) chromedp.WaitVisible(`form[name]`, chromedp.ByQuery), - // Submit without filling fields - chromedp.WaitVisible(`form[name]`, chromedp.ByQuery), - chromedp.Evaluate(` - const form = document.querySelector('form[name]'); - if (form) { - // Bypass HTML5 validation to test server-side validation - form.noValidate = true; - // Reset debug flags - window.__lvtSubmitListenerTriggered = false; - window.__lvtActionFound = null; - form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); - } - `, nil), - // Wait for validation errors to appear (server responds with error messages) + // Bypass HTML5 validation so empty submit reaches the server + chromedp.Evaluate(`document.querySelector('form[name]').noValidate = true`, nil), + // Real click triggers proper WebSocket send (dispatchEvent does not) + chromedp.Click(`dialog#add-modal button[type="submit"]`, chromedp.ByQuery), + + // Wait for validation errors to appear waitFor(` (() => { const form = document.querySelector('form[name]'); if (!form) return false; - // Look for validation error messages (small tags with error text) const smallTags = form.querySelectorAll('small'); return smallTags.length > 0 && Array.from(smallTags).some(el => el.textContent.includes('required') || el.textContent.includes('is required') @@ -835,42 +821,11 @@ func TestCompleteWorkflow_BlogApp(t *testing.T) { })() `, 10*time.Second), - // Check debug flags to see if submit was captured - chromedp.Evaluate(` - (() => { - console.log('[DEBUG] Submit listener triggered: ' + window.__lvtSubmitListenerTriggered); - console.log('[DEBUG] Action found: ' + window.__lvtActionFound); - console.log('[DEBUG] In wrapper: ' + window.__lvtInWrapper); - console.log('[DEBUG] Wrapper element: ' + window.__lvtWrapperElement); - console.log('[DEBUG] Before handleAction: ' + window.__lvtBeforeHandleAction); - console.log('[DEBUG] After handleAction: ' + window.__lvtAfterHandleAction); - return { - listenerTriggered: window.__lvtSubmitListenerTriggered, - actionFound: window.__lvtActionFound, - inWrapper: window.__lvtInWrapper, - beforeHandle: window.__lvtBeforeHandleAction, - afterHandle: window.__lvtAfterHandleAction - }; - })() - `, nil), - - // Check for error messages chromedp.Evaluate(` (() => { const form = document.querySelector('form[name]'); - if (!form) { - console.log('[DEBUG] Form not found!'); - return false; - } - console.log('[DEBUG] Form HTML (first 1000 chars): ' + form.outerHTML.substring(0, 1000)); + if (!form) return false; const smallTags = Array.from(form.querySelectorAll('small')); - console.log('[DEBUG] Found ' + smallTags.length + ' small tags'); - smallTags.forEach(el => console.log('[DEBUG] Small text: ' + el.textContent)); - - // Also check for any elements with aria-invalid - const invalidFields = Array.from(form.querySelectorAll('[aria-invalid="true"]')); - console.log('[DEBUG] Found ' + invalidFields.length + ' invalid fields'); - return smallTags.some(el => el.textContent.includes('required') || el.textContent.includes('is required')); })() `, &errorsVisible), diff --git a/e2e/tutorial_test.go b/e2e/tutorial_test.go index d9bb4022..1b240ffe 100644 --- a/e2e/tutorial_test.go +++ b/e2e/tutorial_test.go @@ -658,8 +658,6 @@ func TestTutorialE2E(t *testing.T) { // Create per-subtest context with individual timeout testCtx, cancel := chromedp.NewContext(ctx) defer cancel() - // Use 180s timeout - validation test does multiple operations and can be slow - // in CI with Docker Chrome. Matches complete_workflow_test.go timeout. testCtx, timeoutCancel := context.WithTimeout(testCtx, 180*time.Second) defer timeoutCancel() @@ -667,17 +665,15 @@ func TestTutorialE2E(t *testing.T) { errorsVisible bool titleErrorText string contentErrorText string - formHTML string ) err := chromedp.Run(testCtx, - // Navigate to /posts chromedp.Navigate(testURL+"/posts"), - waitForWebSocketReady(30*time.Second), // Wait for WebSocket init and first update + waitForWebSocketReady(30*time.Second), chromedp.WaitVisible(`[data-lvt-id]`, chromedp.ByQuery), - validateNoTemplateExpressions("[data-lvt-id]"), // Validate no raw template expressions + validateNoTemplateExpressions("[data-lvt-id]"), - // Open add modal via DOM manipulation (more reliable than click event delegation) + // Open add modal chromedp.WaitVisible(`[command="show-modal"][commandfor="add-modal"]`, chromedp.ByQuery), chromedp.Evaluate(` (() => { @@ -687,18 +683,12 @@ func TestTutorialE2E(t *testing.T) { } })() `, nil), - // Wait for form to be visible (modal is open) chromedp.WaitVisible(`form[name]`, chromedp.ByQuery), - // Submit form WITHOUT filling required fields - chromedp.Evaluate(` - const form = document.querySelector('form[name]'); - if (form) { - // Bypass HTML5 validation to test server-side validation - form.noValidate = true; - form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); - } - `, nil), + // Bypass HTML5 validation so empty submit reaches the server + chromedp.Evaluate(`document.querySelector('form[name]').noValidate = true`, nil), + // Real click triggers proper WebSocket send (dispatchEvent does not) + chromedp.Click(`dialog#add-modal button[type="submit"]`, chromedp.ByQuery), // Wait for validation errors to appear (server responds with error messages) waitFor(` @@ -712,13 +702,9 @@ func TestTutorialE2E(t *testing.T) { })() `, 10*time.Second), - // Debug: Capture the form HTML - chromedp.Evaluate(`document.querySelector('form[name]')?.outerHTML || 'Form not found'`, &formHTML), - - // Check if error messages are visible in the UI (rendered server-side) + // Check if error messages are visible chromedp.Evaluate(` (() => { - // Look for error messages in tags (server-side rendered via .lvt.HasError) const form = document.querySelector('form[name]'); if (!form) return false; const smallTags = Array.from(form.querySelectorAll('small')); @@ -726,12 +712,11 @@ func TestTutorialE2E(t *testing.T) { })() `, &errorsVisible), - // Get specific error texts (server-side rendered) + // Get specific error texts chromedp.Evaluate(` (() => { const form = document.querySelector('form[name]'); if (!form) return ''; - // Find the small tag near the title input const titleDiv = Array.from(form.querySelectorAll('div')).find(div => { const label = div.querySelector('label'); return label && label.textContent.includes('Title'); @@ -743,7 +728,6 @@ func TestTutorialE2E(t *testing.T) { (() => { const form = document.querySelector('form[name]'); if (!form) return ''; - // Find the small tag near the content input const contentDiv = Array.from(form.querySelectorAll('div')).find(div => { const label = div.querySelector('label'); return label && label.textContent.includes('Content'); @@ -756,36 +740,11 @@ func TestTutorialE2E(t *testing.T) { t.Fatalf("Failed to test validation: %v", err) } - // Debug: Check what the client has - var lastWSMessage, clientErrors, activeFormStatus, handleResponseCalled, renderCalled, responseMeta, allWSMessages, errorElementsCount string - chromedp.Run(testCtx, - chromedp.Evaluate(`window.__lastWSMessage || 'No WS message'`, &lastWSMessage), - chromedp.Evaluate(`JSON.stringify(window.liveTemplateClient?.errors || {})`, &clientErrors), - chromedp.Evaluate(`window.liveTemplateClient?.formLifecycleManager?.activeForm ? 'active' : 'not-active'`, &activeFormStatus), - chromedp.Evaluate(`window.__lvtHandleResponseCalled ? 'yes' : 'no'`, &handleResponseCalled), - chromedp.Evaluate(`window.__lvtRenderFieldErrorsCalled ? 'yes' : 'no'`, &renderCalled), - chromedp.Evaluate(`JSON.stringify(window.__lvtResponseMeta || {})`, &responseMeta), - chromedp.Evaluate(`JSON.stringify(window.__wsMessages?.slice(-5) || [])`, &allWSMessages), - chromedp.Evaluate(`document.querySelectorAll('small[data-lvt-error]').length.toString()`, &errorElementsCount), - ) - - t.Logf("Last WS message: %s", lastWSMessage) - t.Logf("All WS messages (last 5): %s", allWSMessages) - t.Logf("Client errors state: %s", clientErrors) - t.Logf("Active form status: %s", activeFormStatus) - t.Logf("HandleResponse called: %s", handleResponseCalled) - t.Logf("RenderFieldErrors called: %s", renderCalled) - t.Logf("Response meta: %s", responseMeta) - t.Logf("Error elements count: %s", errorElementsCount) - t.Logf("Form HTML (first 500 chars): %s", formHTML[:min(500, len(formHTML))]) - - // Verify errors are displayed in the UI (server-side rendered) if !errorsVisible { t.Error("❌ Error messages are not visible in the UI") } t.Log("✅ Error messages are visible in the UI") - // Verify specific field errors if titleErrorText == "" { t.Error("❌ Title field error not displayed") } else { From 940ad33a20b05befb65e5e84b3a07277ffa11cab Mon Sep 17 00:00:00 2001 From: Adnaan Date: Mon, 20 Apr 2026 05:33:32 +0200 Subject: [PATCH 4/6] fix: open dialog via commandfor click instead of showModal() chromedp.Click inside a dialog opened via JavaScript showModal() doesn't properly interact with the top layer. Match the working "Add Post" test pattern: open dialog via clicking the commandfor button, wait for input visibility, then click the submit button. Co-Authored-By: Claude Opus 4.6 --- e2e/complete_workflow_test.go | 19 +++++++------------ e2e/tutorial_test.go | 19 +++++++------------ 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/e2e/complete_workflow_test.go b/e2e/complete_workflow_test.go index 76608f9d..1e3148d1 100644 --- a/e2e/complete_workflow_test.go +++ b/e2e/complete_workflow_test.go @@ -792,22 +792,17 @@ func TestCompleteWorkflow_BlogApp(t *testing.T) { waitForWebSocketReady(5*time.Second), chromedp.WaitVisible(`[data-lvt-id]`, chromedp.ByQuery), - // Open add modal + // Open add modal via real click (matches working "Add Post" pattern) chromedp.WaitVisible(`[command="show-modal"][commandfor="add-modal"]`, chromedp.ByQuery), - chromedp.Evaluate(` - (() => { - const modal = document.querySelector('dialog#add-modal'); - if (modal && !modal.open) { - modal.showModal(); - } - })() - `, nil), - chromedp.WaitVisible(`form[name]`, chromedp.ByQuery), + chromedp.Click(`[command="show-modal"][commandfor="add-modal"]`, chromedp.ByQuery), + waitFor(`document.querySelector('dialog#add-modal')?.open === true`, 10*time.Second), + // Wait for form inputs to be interactive + chromedp.WaitVisible(`input[name="title"]`, chromedp.ByQuery), // Bypass HTML5 validation so empty submit reaches the server chromedp.Evaluate(`document.querySelector('form[name]').noValidate = true`, nil), - // Real click triggers proper WebSocket send (dispatchEvent does not) - chromedp.Click(`dialog#add-modal button[type="submit"]`, chromedp.ByQuery), + // Submit empty form + chromedp.Click(`button[type="submit"]`, chromedp.ByQuery), // Wait for validation errors to appear waitFor(` diff --git a/e2e/tutorial_test.go b/e2e/tutorial_test.go index 1b240ffe..5f5d9194 100644 --- a/e2e/tutorial_test.go +++ b/e2e/tutorial_test.go @@ -673,22 +673,17 @@ func TestTutorialE2E(t *testing.T) { chromedp.WaitVisible(`[data-lvt-id]`, chromedp.ByQuery), validateNoTemplateExpressions("[data-lvt-id]"), - // Open add modal + // Open add modal via real click (matches working "Add Post" pattern) chromedp.WaitVisible(`[command="show-modal"][commandfor="add-modal"]`, chromedp.ByQuery), - chromedp.Evaluate(` - (() => { - const modal = document.querySelector('dialog#add-modal'); - if (modal && !modal.open) { - modal.showModal(); - } - })() - `, nil), - chromedp.WaitVisible(`form[name]`, chromedp.ByQuery), + chromedp.Click(`[command="show-modal"][commandfor="add-modal"]`, chromedp.ByQuery), + waitFor(`document.querySelector('dialog#add-modal')?.open === true`, 10*time.Second), + // Wait for form inputs to be interactive + chromedp.WaitVisible(`input[name="title"]`, chromedp.ByQuery), // Bypass HTML5 validation so empty submit reaches the server chromedp.Evaluate(`document.querySelector('form[name]').noValidate = true`, nil), - // Real click triggers proper WebSocket send (dispatchEvent does not) - chromedp.Click(`dialog#add-modal button[type="submit"]`, chromedp.ByQuery), + // Submit empty form + chromedp.Click(`button[type="submit"]`, chromedp.ByQuery), // Wait for validation errors to appear (server responds with error messages) waitFor(` From a073f0e7eedc08b38bd23f3e81f6f39728896495 Mon Sep 17 00:00:00 2001 From: Adnaan Date: Mon, 20 Apr 2026 09:19:52 +0200 Subject: [PATCH 5/6] Revert "fix: open dialog via commandfor click instead of showModal()" This reverts commit 940ad33a20b05befb65e5e84b3a07277ffa11cab. --- e2e/complete_workflow_test.go | 19 ++++++++++++------- e2e/tutorial_test.go | 19 ++++++++++++------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/e2e/complete_workflow_test.go b/e2e/complete_workflow_test.go index 1e3148d1..76608f9d 100644 --- a/e2e/complete_workflow_test.go +++ b/e2e/complete_workflow_test.go @@ -792,17 +792,22 @@ func TestCompleteWorkflow_BlogApp(t *testing.T) { waitForWebSocketReady(5*time.Second), chromedp.WaitVisible(`[data-lvt-id]`, chromedp.ByQuery), - // Open add modal via real click (matches working "Add Post" pattern) + // Open add modal chromedp.WaitVisible(`[command="show-modal"][commandfor="add-modal"]`, chromedp.ByQuery), - chromedp.Click(`[command="show-modal"][commandfor="add-modal"]`, chromedp.ByQuery), - waitFor(`document.querySelector('dialog#add-modal')?.open === true`, 10*time.Second), + chromedp.Evaluate(` + (() => { + const modal = document.querySelector('dialog#add-modal'); + if (modal && !modal.open) { + modal.showModal(); + } + })() + `, nil), + chromedp.WaitVisible(`form[name]`, chromedp.ByQuery), - // Wait for form inputs to be interactive - chromedp.WaitVisible(`input[name="title"]`, chromedp.ByQuery), // Bypass HTML5 validation so empty submit reaches the server chromedp.Evaluate(`document.querySelector('form[name]').noValidate = true`, nil), - // Submit empty form - chromedp.Click(`button[type="submit"]`, chromedp.ByQuery), + // Real click triggers proper WebSocket send (dispatchEvent does not) + chromedp.Click(`dialog#add-modal button[type="submit"]`, chromedp.ByQuery), // Wait for validation errors to appear waitFor(` diff --git a/e2e/tutorial_test.go b/e2e/tutorial_test.go index 5f5d9194..1b240ffe 100644 --- a/e2e/tutorial_test.go +++ b/e2e/tutorial_test.go @@ -673,17 +673,22 @@ func TestTutorialE2E(t *testing.T) { chromedp.WaitVisible(`[data-lvt-id]`, chromedp.ByQuery), validateNoTemplateExpressions("[data-lvt-id]"), - // Open add modal via real click (matches working "Add Post" pattern) + // Open add modal chromedp.WaitVisible(`[command="show-modal"][commandfor="add-modal"]`, chromedp.ByQuery), - chromedp.Click(`[command="show-modal"][commandfor="add-modal"]`, chromedp.ByQuery), - waitFor(`document.querySelector('dialog#add-modal')?.open === true`, 10*time.Second), + chromedp.Evaluate(` + (() => { + const modal = document.querySelector('dialog#add-modal'); + if (modal && !modal.open) { + modal.showModal(); + } + })() + `, nil), + chromedp.WaitVisible(`form[name]`, chromedp.ByQuery), - // Wait for form inputs to be interactive - chromedp.WaitVisible(`input[name="title"]`, chromedp.ByQuery), // Bypass HTML5 validation so empty submit reaches the server chromedp.Evaluate(`document.querySelector('form[name]').noValidate = true`, nil), - // Submit empty form - chromedp.Click(`button[type="submit"]`, chromedp.ByQuery), + // Real click triggers proper WebSocket send (dispatchEvent does not) + chromedp.Click(`dialog#add-modal button[type="submit"]`, chromedp.ByQuery), // Wait for validation errors to appear (server responds with error messages) waitFor(` From 2e78568f769b72438d9ad81c76878c031d1a17dd Mon Sep 17 00:00:00 2001 From: Adnaan Date: Mon, 20 Apr 2026 09:19:56 +0200 Subject: [PATCH 6/6] Revert "fix: use real click instead of dispatchEvent for validation test" This reverts commit 76bf781b1bd6ec9165a9fe0e9b03bf5141144af8. --- e2e/complete_workflow_test.go | 61 ++++++++++++++++++++++++++++++----- e2e/tutorial_test.go | 59 +++++++++++++++++++++++++++------ 2 files changed, 103 insertions(+), 17 deletions(-) diff --git a/e2e/complete_workflow_test.go b/e2e/complete_workflow_test.go index 76608f9d..6591f4c3 100644 --- a/e2e/complete_workflow_test.go +++ b/e2e/complete_workflow_test.go @@ -779,9 +779,13 @@ func TestCompleteWorkflow_BlogApp(t *testing.T) { }) // Test 11.5: Validation Errors + // Bug was fixed on 2025-10-24 - see BUG-VALIDATION-CONDITIONALS.md:409 t.Run("Validation Errors", func(t *testing.T) { ctx, cancel := createBrowserContext() defer cancel() + // Use 180s timeout - validation test does multiple operations and can be slow + // Running against Docker container adds significant overhead compared to local server. + // Increased from 120s to 180s to handle Docker networking and resource contention. ctx, timeoutCancel := context.WithTimeout(ctx, 180*time.Second) defer timeoutCancel() @@ -792,7 +796,7 @@ func TestCompleteWorkflow_BlogApp(t *testing.T) { waitForWebSocketReady(5*time.Second), chromedp.WaitVisible(`[data-lvt-id]`, chromedp.ByQuery), - // Open add modal + // Open add modal via DOM manipulation (more reliable than click event delegation) chromedp.WaitVisible(`[command="show-modal"][commandfor="add-modal"]`, chromedp.ByQuery), chromedp.Evaluate(` (() => { @@ -802,18 +806,28 @@ func TestCompleteWorkflow_BlogApp(t *testing.T) { } })() `, nil), + // Wait for form to be visible (modal is open) chromedp.WaitVisible(`form[name]`, chromedp.ByQuery), - // Bypass HTML5 validation so empty submit reaches the server - chromedp.Evaluate(`document.querySelector('form[name]').noValidate = true`, nil), - // Real click triggers proper WebSocket send (dispatchEvent does not) - chromedp.Click(`dialog#add-modal button[type="submit"]`, chromedp.ByQuery), - - // Wait for validation errors to appear + // Submit without filling fields + chromedp.WaitVisible(`form[name]`, chromedp.ByQuery), + chromedp.Evaluate(` + const form = document.querySelector('form[name]'); + if (form) { + // Bypass HTML5 validation to test server-side validation + form.noValidate = true; + // Reset debug flags + window.__lvtSubmitListenerTriggered = false; + window.__lvtActionFound = null; + form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); + } + `, nil), + // Wait for validation errors to appear (server responds with error messages) waitFor(` (() => { const form = document.querySelector('form[name]'); if (!form) return false; + // Look for validation error messages (small tags with error text) const smallTags = form.querySelectorAll('small'); return smallTags.length > 0 && Array.from(smallTags).some(el => el.textContent.includes('required') || el.textContent.includes('is required') @@ -821,11 +835,42 @@ func TestCompleteWorkflow_BlogApp(t *testing.T) { })() `, 10*time.Second), + // Check debug flags to see if submit was captured + chromedp.Evaluate(` + (() => { + console.log('[DEBUG] Submit listener triggered: ' + window.__lvtSubmitListenerTriggered); + console.log('[DEBUG] Action found: ' + window.__lvtActionFound); + console.log('[DEBUG] In wrapper: ' + window.__lvtInWrapper); + console.log('[DEBUG] Wrapper element: ' + window.__lvtWrapperElement); + console.log('[DEBUG] Before handleAction: ' + window.__lvtBeforeHandleAction); + console.log('[DEBUG] After handleAction: ' + window.__lvtAfterHandleAction); + return { + listenerTriggered: window.__lvtSubmitListenerTriggered, + actionFound: window.__lvtActionFound, + inWrapper: window.__lvtInWrapper, + beforeHandle: window.__lvtBeforeHandleAction, + afterHandle: window.__lvtAfterHandleAction + }; + })() + `, nil), + + // Check for error messages chromedp.Evaluate(` (() => { const form = document.querySelector('form[name]'); - if (!form) return false; + if (!form) { + console.log('[DEBUG] Form not found!'); + return false; + } + console.log('[DEBUG] Form HTML (first 1000 chars): ' + form.outerHTML.substring(0, 1000)); const smallTags = Array.from(form.querySelectorAll('small')); + console.log('[DEBUG] Found ' + smallTags.length + ' small tags'); + smallTags.forEach(el => console.log('[DEBUG] Small text: ' + el.textContent)); + + // Also check for any elements with aria-invalid + const invalidFields = Array.from(form.querySelectorAll('[aria-invalid="true"]')); + console.log('[DEBUG] Found ' + invalidFields.length + ' invalid fields'); + return smallTags.some(el => el.textContent.includes('required') || el.textContent.includes('is required')); })() `, &errorsVisible), diff --git a/e2e/tutorial_test.go b/e2e/tutorial_test.go index 1b240ffe..d9bb4022 100644 --- a/e2e/tutorial_test.go +++ b/e2e/tutorial_test.go @@ -658,6 +658,8 @@ func TestTutorialE2E(t *testing.T) { // Create per-subtest context with individual timeout testCtx, cancel := chromedp.NewContext(ctx) defer cancel() + // Use 180s timeout - validation test does multiple operations and can be slow + // in CI with Docker Chrome. Matches complete_workflow_test.go timeout. testCtx, timeoutCancel := context.WithTimeout(testCtx, 180*time.Second) defer timeoutCancel() @@ -665,15 +667,17 @@ func TestTutorialE2E(t *testing.T) { errorsVisible bool titleErrorText string contentErrorText string + formHTML string ) err := chromedp.Run(testCtx, + // Navigate to /posts chromedp.Navigate(testURL+"/posts"), - waitForWebSocketReady(30*time.Second), + waitForWebSocketReady(30*time.Second), // Wait for WebSocket init and first update chromedp.WaitVisible(`[data-lvt-id]`, chromedp.ByQuery), - validateNoTemplateExpressions("[data-lvt-id]"), + validateNoTemplateExpressions("[data-lvt-id]"), // Validate no raw template expressions - // Open add modal + // Open add modal via DOM manipulation (more reliable than click event delegation) chromedp.WaitVisible(`[command="show-modal"][commandfor="add-modal"]`, chromedp.ByQuery), chromedp.Evaluate(` (() => { @@ -683,12 +687,18 @@ func TestTutorialE2E(t *testing.T) { } })() `, nil), + // Wait for form to be visible (modal is open) chromedp.WaitVisible(`form[name]`, chromedp.ByQuery), - // Bypass HTML5 validation so empty submit reaches the server - chromedp.Evaluate(`document.querySelector('form[name]').noValidate = true`, nil), - // Real click triggers proper WebSocket send (dispatchEvent does not) - chromedp.Click(`dialog#add-modal button[type="submit"]`, chromedp.ByQuery), + // Submit form WITHOUT filling required fields + chromedp.Evaluate(` + const form = document.querySelector('form[name]'); + if (form) { + // Bypass HTML5 validation to test server-side validation + form.noValidate = true; + form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); + } + `, nil), // Wait for validation errors to appear (server responds with error messages) waitFor(` @@ -702,9 +712,13 @@ func TestTutorialE2E(t *testing.T) { })() `, 10*time.Second), - // Check if error messages are visible + // Debug: Capture the form HTML + chromedp.Evaluate(`document.querySelector('form[name]')?.outerHTML || 'Form not found'`, &formHTML), + + // Check if error messages are visible in the UI (rendered server-side) chromedp.Evaluate(` (() => { + // Look for error messages in tags (server-side rendered via .lvt.HasError) const form = document.querySelector('form[name]'); if (!form) return false; const smallTags = Array.from(form.querySelectorAll('small')); @@ -712,11 +726,12 @@ func TestTutorialE2E(t *testing.T) { })() `, &errorsVisible), - // Get specific error texts + // Get specific error texts (server-side rendered) chromedp.Evaluate(` (() => { const form = document.querySelector('form[name]'); if (!form) return ''; + // Find the small tag near the title input const titleDiv = Array.from(form.querySelectorAll('div')).find(div => { const label = div.querySelector('label'); return label && label.textContent.includes('Title'); @@ -728,6 +743,7 @@ func TestTutorialE2E(t *testing.T) { (() => { const form = document.querySelector('form[name]'); if (!form) return ''; + // Find the small tag near the content input const contentDiv = Array.from(form.querySelectorAll('div')).find(div => { const label = div.querySelector('label'); return label && label.textContent.includes('Content'); @@ -740,11 +756,36 @@ func TestTutorialE2E(t *testing.T) { t.Fatalf("Failed to test validation: %v", err) } + // Debug: Check what the client has + var lastWSMessage, clientErrors, activeFormStatus, handleResponseCalled, renderCalled, responseMeta, allWSMessages, errorElementsCount string + chromedp.Run(testCtx, + chromedp.Evaluate(`window.__lastWSMessage || 'No WS message'`, &lastWSMessage), + chromedp.Evaluate(`JSON.stringify(window.liveTemplateClient?.errors || {})`, &clientErrors), + chromedp.Evaluate(`window.liveTemplateClient?.formLifecycleManager?.activeForm ? 'active' : 'not-active'`, &activeFormStatus), + chromedp.Evaluate(`window.__lvtHandleResponseCalled ? 'yes' : 'no'`, &handleResponseCalled), + chromedp.Evaluate(`window.__lvtRenderFieldErrorsCalled ? 'yes' : 'no'`, &renderCalled), + chromedp.Evaluate(`JSON.stringify(window.__lvtResponseMeta || {})`, &responseMeta), + chromedp.Evaluate(`JSON.stringify(window.__wsMessages?.slice(-5) || [])`, &allWSMessages), + chromedp.Evaluate(`document.querySelectorAll('small[data-lvt-error]').length.toString()`, &errorElementsCount), + ) + + t.Logf("Last WS message: %s", lastWSMessage) + t.Logf("All WS messages (last 5): %s", allWSMessages) + t.Logf("Client errors state: %s", clientErrors) + t.Logf("Active form status: %s", activeFormStatus) + t.Logf("HandleResponse called: %s", handleResponseCalled) + t.Logf("RenderFieldErrors called: %s", renderCalled) + t.Logf("Response meta: %s", responseMeta) + t.Logf("Error elements count: %s", errorElementsCount) + t.Logf("Form HTML (first 500 chars): %s", formHTML[:min(500, len(formHTML))]) + + // Verify errors are displayed in the UI (server-side rendered) if !errorsVisible { t.Error("❌ Error messages are not visible in the UI") } t.Log("✅ Error messages are visible in the UI") + // Verify specific field errors if titleErrorText == "" { t.Error("❌ Title field error not displayed") } else {