From 0f58a82a6c2b386d4990596ef97eeffdfdf7f760 Mon Sep 17 00:00:00 2001 From: Adnaan Date: Sun, 25 Jan 2026 12:52:50 +0100 Subject: [PATCH 1/4] test: add WebSocket CRUD E2E test for progressive-enhancement Adds TestProgressiveEnhancement_WebSocketCRUD that verifies add and delete operations work correctly with DOM updates. This test validates that the auto-generated keys feature works end-to-end. Test sequence: 1. Navigate to page and count initial todos (3) 2. Add a new todo, verify count increases (4) 3. Delete the added todo, verify count returns to initial (3) Co-Authored-By: Claude Opus 4.5 --- .../progressive_enhancement_test.go | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/progressive-enhancement/progressive_enhancement_test.go b/progressive-enhancement/progressive_enhancement_test.go index dc2b04c..ab0b73c 100644 --- a/progressive-enhancement/progressive_enhancement_test.go +++ b/progressive-enhancement/progressive_enhancement_test.go @@ -486,6 +486,78 @@ func TestProgressiveEnhancement_DisabledReturnsJSON(t *testing.T) { } } +// TestProgressiveEnhancement_WebSocketCRUD tests add, toggle, and delete operations via WebSocket +// with DOM verification to ensure the UI updates correctly. +func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { + server := setupServer(t) + defer server.Close() + + // Create browser context + opts := append(chromedp.DefaultExecAllocatorOptions[:], + chromedp.Flag("headless", true), + ) + allocCtx, allocCancel := chromedp.NewExecAllocator(context.Background(), opts...) + defer allocCancel() + + ctx, cancel := chromedp.NewContext(allocCtx) + defer cancel() + + ctx, cancel = context.WithTimeout(ctx, 15*time.Second) + defer cancel() + + var initialCount int + var afterAddCount int + var afterDeleteCount int + + // Step 1: Navigate and count initial todos + err := chromedp.Run(ctx, + chromedp.Navigate(server.URL), + chromedp.WaitReady(`body`, chromedp.ByQuery), + chromedp.Sleep(1*time.Second), // Wait for WebSocket to connect + chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &initialCount), + ) + if err != nil { + t.Fatalf("Step 1 (navigate) error: %v", err) + } + t.Logf("Initial todo count: %d", initialCount) + + // Step 2: Add a new todo + err = chromedp.Run(ctx, + chromedp.Clear(`input[name="title"]`, chromedp.ByQuery), + chromedp.SendKeys(`input[name="title"]`, "E2E Test Todo", chromedp.ByQuery), + chromedp.Submit(`form[lvt-submit="add"]`, chromedp.ByQuery), + chromedp.Sleep(1*time.Second), // Wait for WebSocket response + chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterAddCount), + ) + if err != nil { + t.Fatalf("Step 2 (add) error: %v", err) + } + t.Logf("After add count: %d", afterAddCount) + + // Verify add worked + if afterAddCount != initialCount+1 { + t.Errorf("Add failed: expected %d todos, got %d", initialCount+1, afterAddCount) + } + + // Step 3: Delete the last todo (the one we just added) + err = chromedp.Run(ctx, + chromedp.Submit(`.todo-item:last-child form[lvt-submit="delete"]`, chromedp.ByQuery), + chromedp.Sleep(1*time.Second), // Wait for WebSocket response + chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterDeleteCount), + ) + if err != nil { + t.Fatalf("Step 3 (delete) error: %v", err) + } + t.Logf("After delete count: %d", afterDeleteCount) + + // Verify delete worked + if afterDeleteCount != initialCount { + t.Errorf("Delete failed: expected %d todos, got %d", initialCount, afterDeleteCount) + } + + t.Log("SUCCESS: WebSocket CRUD operations work correctly with DOM updates") +} + func init() { // Suppress log output during tests fmt.Println("Progressive Enhancement E2E Tests") From 3882e5b5cb0337e1462425b95770b77b8de0f0ef Mon Sep 17 00:00:00 2001 From: Adnaan Date: Sun, 25 Jan 2026 12:58:54 +0100 Subject: [PATCH 2/4] test: add toggle verification to WebSocket CRUD test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expands TestProgressiveEnhancement_WebSocketCRUD to verify all operations: 1. Add new todo (count: 3 → 4) 2. Toggle to mark complete (verify .completed class added) 3. Toggle again to mark incomplete (verify .completed class removed) 4. Delete the todo (count: 4 → 3) This comprehensive test validates that the auto-generated keys feature correctly handles in-place updates, not just additions and deletions. Co-Authored-By: Claude Opus 4.5 --- .../progressive_enhancement_test.go | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/progressive-enhancement/progressive_enhancement_test.go b/progressive-enhancement/progressive_enhancement_test.go index ab0b73c..87f97fc 100644 --- a/progressive-enhancement/progressive_enhancement_test.go +++ b/progressive-enhancement/progressive_enhancement_test.go @@ -502,11 +502,15 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { ctx, cancel := chromedp.NewContext(allocCtx) defer cancel() - ctx, cancel = context.WithTimeout(ctx, 15*time.Second) + ctx, cancel = context.WithTimeout(ctx, 30*time.Second) defer cancel() var initialCount int var afterAddCount int + var afterToggleCount int + var hasCompletedClass bool + var afterUntoggleCount int + var hasCompletedClassAfterUntoggle bool var afterDeleteCount int // Step 1: Navigate and count initial todos @@ -539,14 +543,55 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { t.Errorf("Add failed: expected %d todos, got %d", initialCount+1, afterAddCount) } - // Step 3: Delete the last todo (the one we just added) + // Step 3: Toggle the last todo (mark as complete) + // The new todo should NOT have .completed class initially + err = chromedp.Run(ctx, + chromedp.Submit(`.todo-item:last-child form[lvt-submit="toggle"]`, chromedp.ByQuery), + chromedp.Sleep(1*time.Second), // Wait for WebSocket response + chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterToggleCount), + chromedp.Evaluate(`document.querySelector('.todo-item:last-child').classList.contains('completed')`, &hasCompletedClass), + ) + if err != nil { + t.Fatalf("Step 3 (toggle) error: %v", err) + } + t.Logf("After toggle count: %d, has completed class: %v", afterToggleCount, hasCompletedClass) + + // Verify toggle worked - count should stay the same, but item should now have .completed class + if afterToggleCount != afterAddCount { + t.Errorf("Toggle changed item count: expected %d, got %d", afterAddCount, afterToggleCount) + } + if !hasCompletedClass { + t.Errorf("Toggle failed: item should have .completed class after toggle") + } + + // Step 4: Toggle again (mark as incomplete) + err = chromedp.Run(ctx, + chromedp.Submit(`.todo-item:last-child form[lvt-submit="toggle"]`, chromedp.ByQuery), + chromedp.Sleep(1*time.Second), // Wait for WebSocket response + chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterUntoggleCount), + chromedp.Evaluate(`document.querySelector('.todo-item:last-child').classList.contains('completed')`, &hasCompletedClassAfterUntoggle), + ) + if err != nil { + t.Fatalf("Step 4 (untoggle) error: %v", err) + } + t.Logf("After untoggle count: %d, has completed class: %v", afterUntoggleCount, hasCompletedClassAfterUntoggle) + + // Verify untoggle worked - count should stay the same, item should NOT have .completed class + if afterUntoggleCount != afterAddCount { + t.Errorf("Untoggle changed item count: expected %d, got %d", afterAddCount, afterUntoggleCount) + } + if hasCompletedClassAfterUntoggle { + t.Errorf("Untoggle failed: item should NOT have .completed class after second toggle") + } + + // Step 5: Delete the last todo (the one we just added) err = chromedp.Run(ctx, chromedp.Submit(`.todo-item:last-child form[lvt-submit="delete"]`, chromedp.ByQuery), chromedp.Sleep(1*time.Second), // Wait for WebSocket response chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterDeleteCount), ) if err != nil { - t.Fatalf("Step 3 (delete) error: %v", err) + t.Fatalf("Step 5 (delete) error: %v", err) } t.Logf("After delete count: %d", afterDeleteCount) @@ -555,7 +600,7 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { t.Errorf("Delete failed: expected %d todos, got %d", initialCount, afterDeleteCount) } - t.Log("SUCCESS: WebSocket CRUD operations work correctly with DOM updates") + t.Log("SUCCESS: All CRUD operations (add, toggle, untoggle, delete) work correctly") } func init() { From 1bc43967f2905da682b35af2292b25e92bfb1513 Mon Sep 17 00:00:00 2001 From: Adnaan Date: Sun, 25 Jan 2026 17:54:42 +0100 Subject: [PATCH 3/4] test: add delete-then-toggle E2E test for progressive-enhancement Verifies that auto-generated keys work correctly after item deletion. The test deletes one item and then toggles a different item to ensure key-based DOM updates work correctly after range modifications. Co-Authored-By: Claude Opus 4.5 --- .../progressive_enhancement_test.go | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/progressive-enhancement/progressive_enhancement_test.go b/progressive-enhancement/progressive_enhancement_test.go index 87f97fc..6caef23 100644 --- a/progressive-enhancement/progressive_enhancement_test.go +++ b/progressive-enhancement/progressive_enhancement_test.go @@ -603,6 +603,97 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { t.Log("SUCCESS: All CRUD operations (add, toggle, untoggle, delete) work correctly") } +// TestProgressiveEnhancement_DeleteThenToggle tests toggling a todo after deleting another one. +// This specifically tests that the auto-generated keys work correctly after item removal. +func TestProgressiveEnhancement_DeleteThenToggle(t *testing.T) { + server := setupServer(t) + defer server.Close() + + // Create browser context + opts := append(chromedp.DefaultExecAllocatorOptions[:], + chromedp.Flag("headless", true), + ) + allocCtx, allocCancel := chromedp.NewExecAllocator(context.Background(), opts...) + defer allocCancel() + + ctx, cancel := chromedp.NewContext(allocCtx) + defer cancel() + + ctx, cancel = context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + var initialCount int + var afterDeleteCount int + var afterToggleCount int + var hasCompletedClass bool + + // Step 1: Navigate and count initial todos + err := chromedp.Run(ctx, + chromedp.Navigate(server.URL), + chromedp.WaitReady(`body`, chromedp.ByQuery), + chromedp.Sleep(1*time.Second), // Wait for WebSocket to connect + chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &initialCount), + ) + if err != nil { + t.Fatalf("Step 1 (navigate) error: %v", err) + } + t.Logf("Initial todo count: %d", initialCount) + + if initialCount < 2 { + t.Fatalf("Need at least 2 initial items for this test, got %d", initialCount) + } + + // Step 2: Delete the FIRST todo + err = chromedp.Run(ctx, + chromedp.Submit(`.todo-item:first-child form[lvt-submit="delete"]`, chromedp.ByQuery), + chromedp.Sleep(1*time.Second), // Wait for WebSocket response + chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterDeleteCount), + ) + if err != nil { + t.Fatalf("Step 2 (delete) error: %v", err) + } + t.Logf("After delete count: %d", afterDeleteCount) + + // Verify delete worked + if afterDeleteCount != initialCount-1 { + t.Errorf("Delete failed: expected %d todos, got %d", initialCount-1, afterDeleteCount) + } + + // Step 3: Toggle the LAST remaining todo (different item than what we deleted) + // 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), + ) + if err != nil { + t.Fatalf("Step 3 (check before toggle) error: %v", err) + } + t.Logf("Last item has completed class before toggle: %v", hasCompletedClassBefore) + + err = chromedp.Run(ctx, + chromedp.Submit(`.todo-item:last-child form[lvt-submit="toggle"]`, chromedp.ByQuery), + chromedp.Sleep(1*time.Second), // Wait for WebSocket response + chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterToggleCount), + chromedp.Evaluate(`document.querySelector('.todo-item:last-child').classList.contains('completed')`, &hasCompletedClass), + ) + if err != nil { + t.Fatalf("Step 3 (toggle) error: %v", err) + } + t.Logf("After toggle count: %d, has completed class: %v", afterToggleCount, hasCompletedClass) + + // Verify toggle worked - count should stay the same + if afterToggleCount != afterDeleteCount { + t.Errorf("Toggle changed item count: expected %d, got %d", afterDeleteCount, afterToggleCount) + } + + // Verify the completed class changed + if hasCompletedClass == hasCompletedClassBefore { + t.Errorf("Toggle failed: completed class should have changed from %v to %v", hasCompletedClassBefore, !hasCompletedClassBefore) + } + + t.Log("SUCCESS: Toggle works correctly after deleting a different item") +} + func init() { // Suppress log output during tests fmt.Println("Progressive Enhancement E2E Tests") From 5f022ed400bd04551e9e0ab2d2fd681f167c7e17 Mon Sep 17 00:00:00 2001 From: Adnaan Date: Sun, 25 Jan 2026 18:32:56 +0100 Subject: [PATCH 4/4] fix: address Copilot review comments for E2E tests - Fix context cancel variable shadowing (use timeoutCancel) - Replace chromedp.Sleep with e2etest.WaitFor for robust waiting - Wait for WebSocket ready before operations - Wait for DOM changes using condition-based waits Co-Authored-By: Claude Opus 4.5 --- .../progressive_enhancement_test.go | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/progressive-enhancement/progressive_enhancement_test.go b/progressive-enhancement/progressive_enhancement_test.go index 6caef23..6a148a8 100644 --- a/progressive-enhancement/progressive_enhancement_test.go +++ b/progressive-enhancement/progressive_enhancement_test.go @@ -502,8 +502,8 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { ctx, cancel := chromedp.NewContext(allocCtx) defer cancel() - ctx, cancel = context.WithTimeout(ctx, 30*time.Second) - defer cancel() + ctx, timeoutCancel := context.WithTimeout(ctx, 30*time.Second) + defer timeoutCancel() var initialCount int var afterAddCount int @@ -513,11 +513,11 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { var hasCompletedClassAfterUntoggle bool var afterDeleteCount int - // Step 1: Navigate and count initial todos + // Step 1: Navigate and wait for WebSocket to connect err := chromedp.Run(ctx, chromedp.Navigate(server.URL), chromedp.WaitReady(`body`, chromedp.ByQuery), - chromedp.Sleep(1*time.Second), // Wait for WebSocket to connect + e2etest.WaitFor(`window.liveTemplateClient && window.liveTemplateClient.isReady()`, 5*time.Second), chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &initialCount), ) if err != nil { @@ -526,11 +526,13 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { t.Logf("Initial todo count: %d", initialCount) // Step 2: Add a new todo + expectedAfterAdd := initialCount + 1 err = chromedp.Run(ctx, chromedp.Clear(`input[name="title"]`, chromedp.ByQuery), chromedp.SendKeys(`input[name="title"]`, "E2E Test Todo", chromedp.ByQuery), chromedp.Submit(`form[lvt-submit="add"]`, chromedp.ByQuery), - chromedp.Sleep(1*time.Second), // Wait for WebSocket response + // 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), ) if err != nil { @@ -539,15 +541,16 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { t.Logf("After add count: %d", afterAddCount) // Verify add worked - if afterAddCount != initialCount+1 { - t.Errorf("Add failed: expected %d todos, got %d", initialCount+1, afterAddCount) + if afterAddCount != expectedAfterAdd { + t.Errorf("Add failed: expected %d todos, got %d", expectedAfterAdd, afterAddCount) } // Step 3: Toggle the last todo (mark as complete) // The new todo should NOT have .completed class initially err = chromedp.Run(ctx, chromedp.Submit(`.todo-item:last-child form[lvt-submit="toggle"]`, chromedp.ByQuery), - chromedp.Sleep(1*time.Second), // Wait for WebSocket response + // 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), ) @@ -567,7 +570,8 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { // Step 4: Toggle again (mark as incomplete) err = chromedp.Run(ctx, chromedp.Submit(`.todo-item:last-child form[lvt-submit="toggle"]`, chromedp.ByQuery), - chromedp.Sleep(1*time.Second), // Wait for WebSocket response + // 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), ) @@ -587,7 +591,8 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { // Step 5: Delete the last todo (the one we just added) err = chromedp.Run(ctx, chromedp.Submit(`.todo-item:last-child form[lvt-submit="delete"]`, chromedp.ByQuery), - chromedp.Sleep(1*time.Second), // Wait for WebSocket response + // 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), ) if err != nil { @@ -619,19 +624,19 @@ func TestProgressiveEnhancement_DeleteThenToggle(t *testing.T) { ctx, cancel := chromedp.NewContext(allocCtx) defer cancel() - ctx, cancel = context.WithTimeout(ctx, 30*time.Second) - defer cancel() + ctx, timeoutCancel := context.WithTimeout(ctx, 30*time.Second) + defer timeoutCancel() var initialCount int var afterDeleteCount int var afterToggleCount int var hasCompletedClass bool - // Step 1: Navigate and count initial todos + // Step 1: Navigate and wait for WebSocket to connect err := chromedp.Run(ctx, chromedp.Navigate(server.URL), chromedp.WaitReady(`body`, chromedp.ByQuery), - chromedp.Sleep(1*time.Second), // Wait for WebSocket to connect + e2etest.WaitFor(`window.liveTemplateClient && window.liveTemplateClient.isReady()`, 5*time.Second), chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &initialCount), ) if err != nil { @@ -644,9 +649,11 @@ func TestProgressiveEnhancement_DeleteThenToggle(t *testing.T) { } // Step 2: Delete the FIRST todo + expectedAfterDelete := initialCount - 1 err = chromedp.Run(ctx, chromedp.Submit(`.todo-item:first-child form[lvt-submit="delete"]`, chromedp.ByQuery), - chromedp.Sleep(1*time.Second), // Wait for WebSocket response + // 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), ) if err != nil { @@ -655,8 +662,8 @@ func TestProgressiveEnhancement_DeleteThenToggle(t *testing.T) { t.Logf("After delete count: %d", afterDeleteCount) // Verify delete worked - if afterDeleteCount != initialCount-1 { - t.Errorf("Delete failed: expected %d todos, got %d", initialCount-1, afterDeleteCount) + if afterDeleteCount != expectedAfterDelete { + t.Errorf("Delete failed: expected %d todos, got %d", expectedAfterDelete, afterDeleteCount) } // Step 3: Toggle the LAST remaining todo (different item than what we deleted) @@ -670,9 +677,16 @@ 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')` + if hasCompletedClassBefore { + toggleWaitCondition = `!document.querySelector('.todo-item:last-child').classList.contains('completed')` + } + err = chromedp.Run(ctx, chromedp.Submit(`.todo-item:last-child form[lvt-submit="toggle"]`, chromedp.ByQuery), - chromedp.Sleep(1*time.Second), // Wait for WebSocket response + // 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), )