diff --git a/avatar-upload/avatar-upload_test.go b/avatar-upload/avatar-upload_test.go index e5645bc..9854bbe 100644 --- a/avatar-upload/avatar-upload_test.go +++ b/avatar-upload/avatar-upload_test.go @@ -7,8 +7,6 @@ import ( "fmt" "net/http" "net/http/httptest" - "os" - "path/filepath" "strings" "testing" "time" @@ -92,172 +90,125 @@ func TestAvatarUploadE2E(t *testing.T) { t.Log("✅ Initial page load verified") }) - t.Run("Upload Avatar - Live Update", func(t *testing.T) { - t.Skip("SKIP: chromedp.SetUploadFiles() doesn't populate input.files in DOM, making programmatic file upload testing unreliable. Upload functionality verified via manual browser testing.") + 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. - // Create a test image file - testImagePath, err := createTestImage(t) - if err != nil { - t.Fatalf("Failed to create test image: %v", err) - } - defer os.Remove(testImagePath) - - var successText string + err := chromedp.Run(ctx, + // Fresh page load + chromedp.Navigate(e2etest.GetChromeTestURL(serverPort)), + e2etest.WaitForWebSocketReady(5*time.Second), - // Read the test image file - imageData, err := os.ReadFile(testImagePath) + // Send upload_start via the client's WebSocket, then upload chunks, + // send upload_complete, and finally submit the form action. + 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}] + }); + })() + `, nil), + + // Wait for the "Upload complete!" message via live WebSocket update + e2etest.WaitFor(`document.querySelector('ins') !== null`, 15*time.Second), + ) if err != nil { - t.Fatalf("Failed to read test image: %v", err) + 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, - // Wait for the file input to be ready - chromedp.WaitReady(`input[type="file"][lvt-upload="avatar"]`, chromedp.ByQuery), - - // Simulate file selection by creating a File object and triggering the upload handler directly - // chromedp.SetUploadFiles doesn't populate input.files, so we need to do this programmatically - chromedp.Evaluate(fmt.Sprintf(` - (() => { - // Create a File object from base64 data - const base64Data = '%s'; - const binaryData = atob(base64Data); - const bytes = new Uint8Array(binaryData.length); - for (let i = 0; i < binaryData.length; i++) { - bytes[i] = binaryData.charCodeAt(i); - } - const blob = new Blob([bytes], { type: 'image/png' }); - const file = new File([blob], 'test-avatar.png', { type: 'image/png' }); - - // Get the file input and create a FileList-like object - const input = document.querySelector('input[type="file"][lvt-upload="avatar"]'); - if (!input) { - console.error('[TEST] File input not found!'); - return; - } - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(file); - input.files = dataTransfer.files; - - // Trigger the change event - input.dispatchEvent(new Event('change', { bubbles: true })); - - console.log('[TEST] File upload simulated, files:', input.files.length); - })(); - `, base64.StdEncoding.EncodeToString(imageData)), nil), - - // Small delay to let the event propagate - chromedp.Sleep(500*time.Millisecond), - - // Wait for the upload entry to appear (indicates upload_start response received) - // This shows the file is ready but not yet uploading (progress at 0%) - chromedp.WaitVisible(`.upload-entry`, chromedp.ByQuery), - - // Click the "Save Profile" button to trigger upload (AutoUpload is false) - chromedp.Click(`button[type="submit"]`, chromedp.ByQuery), - - // Wait for the "Upload complete!" message to appear LIVE (not after reload!) - // This is the key test - it should appear immediately via WebSocket update - chromedp.WaitVisible(`.success`, chromedp.ByQuery), - - // Get the success message text to verify it's correct - chromedp.TextContent(`.success`, &successText, chromedp.ByQuery), + chromedp.TextContent(`ins`, &successText, chromedp.ByQuery), ) - if err != nil { - t.Fatalf("Failed to upload and verify: %v", err) + t.Fatalf("Failed to read success message: %v", err) } - - // Verify the success message text if !strings.Contains(successText, "Upload complete") { - t.Errorf("Expected success message to contain 'Upload complete', got: %s", successText) + t.Errorf("Expected 'Upload complete' in success message, got: %q", successText) } - t.Log("✅ Upload live update verified - message appeared without page reload") - }) - - t.Run("Upload Progress Display", func(t *testing.T) { - t.Skip("SKIP: chromedp file upload limitation - see Upload Avatar test for details") - - // Create another test image - testImagePath, err := createTestImage(t) - if err != nil { - t.Fatalf("Failed to create test image: %v", err) - } - defer os.Remove(testImagePath) - - var progressHTML string - - // Read the test image file - imageData, err := os.ReadFile(testImagePath) + // 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("Failed to read test image: %v", err) + t.Fatalf("Avatar image did not appear after upload: %v", err) } + // Verify the progress bar shows 100% + var progressVal string err = chromedp.Run(ctx, - // Clear any previous uploads by reloading - chromedp.Navigate(e2etest.GetChromeTestURL(serverPort)), - e2etest.WaitForWebSocketReady(3*time.Second), - chromedp.WaitReady(`input[type="file"][lvt-upload="avatar"]`, chromedp.ByQuery), - - // Simulate file selection programmatically - chromedp.Evaluate(fmt.Sprintf(` - (() => { - const base64Data = '%s'; - const binaryData = atob(base64Data); - const bytes = new Uint8Array(binaryData.length); - for (let i = 0; i < binaryData.length; i++) { - bytes[i] = binaryData.charCodeAt(i); - } - const blob = new Blob([bytes], { type: 'image/png' }); - const file = new File([blob], 'test-avatar.png', { type: 'image/png' }); - - const input = document.querySelector('input[type="file"][lvt-upload="avatar"]'); - if (!input) { - console.error('[TEST] File input not found!'); - return; - } - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(file); - input.files = dataTransfer.files; - - input.dispatchEvent(new Event('change', { bubbles: true })); - - console.log('[TEST] File upload simulated, files:', input.files.length); - })(); - `, base64.StdEncoding.EncodeToString(imageData)), nil), - - // Small delay to let the event propagate - chromedp.Sleep(500*time.Millisecond), - - // Wait for the upload entry to appear (indicates upload_start response received) - chromedp.WaitVisible(`.upload-entry`, chromedp.ByQuery), - - // Click the "Save Profile" button to trigger upload (AutoUpload is false) - chromedp.Click(`button[type="submit"]`, chromedp.ByQuery), - - // Wait for upload to complete - chromedp.WaitVisible(`.success`, chromedp.ByQuery), - - // Get the upload preview HTML - chromedp.OuterHTML(`.upload-preview`, &progressHTML, chromedp.ByQuery), + chromedp.AttributeValue(`progress`, "value", &progressVal, nil, chromedp.ByQuery), ) - if err != nil { - t.Fatalf("Failed to check progress display: %v", err) + t.Fatalf("Failed to read progress bar value: %v", err) } - - // Verify progress elements are present - if !strings.Contains(progressHTML, "upload-entry") { - t.Error("Upload entry not found") - } - if !strings.Contains(progressHTML, "100%") { - t.Error("100% progress not found") - } - if !strings.Contains(progressHTML, "✅ Upload complete!") { - t.Error("Success message not found in HTML") + if progressVal != "100" { + t.Errorf("Expected progress value 100, got: %s", progressVal) } - t.Log("✅ Upload progress display verified") + t.Log("Upload complete, success message shown, avatar image rendered, progress at 100%") }) t.Run("WebSocket Connection", func(t *testing.T) { @@ -275,40 +226,6 @@ func TestAvatarUploadE2E(t *testing.T) { }) } -// createTestImage creates a small test PNG image file -func createTestImage(t *testing.T) (string, error) { - // Simple 1x1 PNG (red pixel) - // PNG signature + IHDR + IDAT + IEND - pngData := []byte{ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature - 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, - 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, - 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, // IDAT chunk - 0x54, 0x08, 0x99, 0x63, 0xF8, 0x0F, 0x00, 0x00, - 0x01, 0x01, 0x00, 0x05, 0x18, 0x0D, 0xA8, 0xDB, - 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, // IEND chunk - 0xAE, 0x42, 0x60, 0x82, - } - - tmpFile, err := os.CreateTemp("", "test-avatar-*.png") - if err != nil { - return "", err - } - defer tmpFile.Close() - - if _, err := tmpFile.Write(pngData); err != nil { - return "", err - } - - absPath, err := filepath.Abs(tmpFile.Name()) - if err != nil { - return "", err - } - - return absPath, nil -} - // ========== WebSocket Tests ========== // TestUploadViaWebSocket tests the complete upload flow via WebSocket diff --git a/live-preview/live_preview_test.go b/live-preview/live_preview_test.go index 5621f5f..b64f293 100644 --- a/live-preview/live_preview_test.go +++ b/live-preview/live_preview_test.go @@ -286,13 +286,28 @@ func TestLivePreviewE2E(t *testing.T) { // After Auto-Wire Input, input has "World" and preview has "Hello, World!" // Type additional characters and verify they append (not prepend due to cursor reset) err := chromedp.Run(ctx, - // Input should still have focus from previous test; type "XY" + // Focus the input and move cursor to end. chromedp.Click dispatches at + // the element center, which lands mid-text and would insert "XY" in the + // middle of "World". Setting selectionStart/End ensures cursor is at the + // end regardless of font metrics or click coordinates. chromedp.Click(`#name-input`, chromedp.ByQuery), + chromedp.Evaluate(`(() => { + const el = document.getElementById('name-input'); + el.selectionStart = el.selectionEnd = el.value.length; + })()`, nil), chromedp.SendKeys(`#name-input`, "XY", chromedp.ByQuery), - // Wait for debounce + round-trip — preview should show "Hello, WorldXY!" - e2etest.WaitForText("#preview", "Hello, WorldXY!", 5*time.Second), + // Wait for debounce (300ms) + round-trip + DOM update. + // CI runners can be slow, so allow extra headroom. + e2etest.WaitForText("#preview", "Hello, WorldXY!", 10*time.Second), ) if err != nil { + // Capture actual state for CI debugging + var inputVal, previewText string + _ = chromedp.Run(ctx, + chromedp.Evaluate(`document.getElementById('name-input').value`, &inputVal), + chromedp.TextContent(`#preview`, &previewText, chromedp.ByQuery), + ) + t.Logf("DEBUG input value: %q, preview text: %q", inputVal, previewText) t.Fatalf("Failed to type additional characters: %v", err) }