Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 98 additions & 181 deletions avatar-upload/avatar-upload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -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 <ins> "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) {
Expand All @@ -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
Expand Down
21 changes: 18 additions & 3 deletions live-preview/live_preview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Loading