A comprehensive e2e testing framework for LiveTemplate applications that reduces boilerplate by 85-90%.
go get github.com/livetemplate/livetemplate/cmd/lvt/testing
package main
import (
"testing"
lvttest "github.com/livetemplate/livetemplate/cmd/lvt/testing"
)
func TestMyApp(t *testing.T) {
// Setup test environment (Chrome + Server)
test := lvttest.Setup(t, &lvttest.SetupOptions{
AppPath: "./main.go",
})
defer test.Cleanup()
// Navigate to page
test.Navigate("/")
// Create assertion helper
assert := lvttest.NewAssert(test)
// Run assertions
assert.PageContains("Welcome")
assert.WebSocketConnected()
assert.NoConsoleErrors()
}
// Browser console logs
test.Console.GetLogs()
test.Console.GetErrors()
test.Console.PrintErrors()
// Server logs
test.Server.FindLog("pattern")
test.Server.PrintLast(10)
// WebSocket messages
test.WebSocket.GetMessages()
test.WebSocket.CountByDirection("sent")
test.WebSocket.Print()
assert := lvttest.NewAssert(test)
// Content
assert.PageContains("text")
assert.PageNotContains("text")
// Elements
assert.ElementExists("selector")
assert.ElementNotExists("selector")
assert.ElementVisible("selector")
assert.ElementHidden("selector")
assert.ElementCount("selector", 5)
// Text
assert.TextContent("selector", "exact text")
assert.TextContains("selector", "substring")
// Attributes & Classes
assert.AttributeValue("selector", "data-id", "123")
assert.HasClass("selector", "active")
assert.NotHasClass("selector", "disabled")
// Tables
assert.TableRowCount(10)
// Forms
assert.FormFieldValue("input[name='email']", "test@example.com")
// Validation
assert.WebSocketConnected()
assert.NoTemplateErrors()
assert.NoConsoleErrors()
crud := lvttest.NewCRUDTester(test, "/products")
// Create with typed fields
crud.Create(
lvttest.TextField("name", "Widget"),
lvttest.FloatField("price", 29.99),
lvttest.IntField("quantity", 100),
lvttest.BoolField("enabled", true),
)
// Verify existence
crud.VerifyExists("Widget")
// Delete
crud.Delete("record-id")
modal := lvttest.NewModalTester(test).
WithModalSelector("[data-test-id='create-modal']")
// Open by action
modal.OpenByAction("open_create")
// Verify visibility
modal.VerifyVisible()
// Fill form
modal.FillForm(
lvttest.TextField("name", "New Item"),
)
// Click button
modal.ClickButton("Create")
// Wait for close
modal.WaitForClose(2 * time.Second)
test := lvttest.Setup(t, &lvttest.SetupOptions{
AppPath: "./main.go",
ChromeMode: lvttest.ChromeDocker, // default
})
test := lvttest.Setup(t, &lvttest.SetupOptions{
AppPath: "./main.go",
ChromeMode: lvttest.ChromeLocal,
ChromePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
})
// All field types for form filling
lvttest.TextField("name", "value")
lvttest.TextAreaField("description", "long text")
lvttest.IntField("quantity", 42)
lvttest.FloatField("price", 19.99)
lvttest.BoolField("enabled", true)
lvttest.SelectField("category", "Electronics")
See examples/testing/ for complete examples:
01_basic/ - Simple smoke test02_crud/ - Full CRUD operations03_debugging/ - Console & debugging04_assertions/ - All assertion types05_modal/ - Modal interactionsBefore (Manual Setup):
// ~100 lines of boilerplate
func TestManual(t *testing.T) {
serverPort, _ := e2etest.GetFreePort()
chromePort, _ := e2etest.GetFreePort()
serverCmd := exec.Command("go", "run", "main.go")
serverCmd.Env = append([]string{"PORT=" + fmt.Sprintf("%d", serverPort)}, ...)
serverCmd.Start()
defer serverCmd.Process.Kill()
time.Sleep(2 * time.Second)
chromeCmd := e2etest.StartDockerChrome(t, chromePort)
defer e2etest.StopDockerChrome(t, chromeCmd, chromePort)
allocCtx, allocCancel := chromedp.NewRemoteAllocator(...)
defer allocCancel()
ctx, cancel := chromedp.NewContext(allocCtx, ...)
defer cancel()
ctx, cancel = context.WithTimeout(ctx, 60*time.Second)
defer cancel()
var html string
chromedp.Run(ctx,
chromedp.Navigate(fmt.Sprintf("http://localhost:%d", serverPort)),
chromedp.WaitVisible("h1", chromedp.ByQuery),
chromedp.OuterHTML("body", &html, chromedp.ByQuery),
)
if !strings.Contains(html, "Welcome") {
t.Error("Page title not found")
}
}
After (With Framework):
// ~10 lines - 90% reduction!
func TestFramework(t *testing.T) {
test := lvttest.Setup(t, &lvttest.SetupOptions{
AppPath: "./main.go",
})
defer test.Cleanup()
test.Navigate("/")
assert := lvttest.NewAssert(test)
assert.PageContains("Welcome")
}
When using lvt gen, tests are automatically generated with the framework:
lvt gen products name price:float quantity:int
Generates:
func TestProductsE2E(t *testing.T) {
test := lvttest.Setup(t, &lvttest.SetupOptions{
AppPath: "../../cmd/myapp/main.go",
})
defer test.Cleanup()
test.Navigate("/products")
assert := lvttest.NewAssert(test)
crud := lvttest.NewCRUDTester(test, "/products")
// Automatic CRUD testing
crud.Create(
lvttest.TextField("name", "Test Product"),
lvttest.FloatField("price", 29.99),
lvttest.IntField("quantity", 100),
)
crud.VerifyExists("Test Product")
}
Skip in short mode:
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
Use subtests: Group related tests with t.Run()
Print debug info on failure:
if err := assert.NoConsoleErrors(); err != nil {
test.Console.PrintErrors()
test.WebSocket.PrintLast(10)
t.Error(err)
}
Clean up test data: Use defer for cleanup operations
Use descriptive test names: Make failures easy to understand
# Restart Docker
docker restart <chrome-container>
# Check logs
docker logs <chrome-container>
test.Server.Print()WaitFor* methods instead of Sleeptest.Console.PrintErrors()test.WebSocket.Print()E2ETest
Context - chromedp contextServerPort - allocated server portChromePort - allocated Chrome debug portConsole - ConsoleLoggerServer - ServerLoggerWebSocket - WSMessageLoggerSetupOptions
AppPath (required) - Path to main.goPort - Server port (auto if 0)Timeout - Test timeout (default 60s)ChromeMode - Docker/Local/SharedChromePath - Path to Chrome binaryAssert
error (nil on success)T.Helper() for proper error reportingCRUDTester
Create(fields ...Field) - Create recordEdit(id, fields ...Field) - Update recordDelete(id) - Delete recordVerifyExists(text) - Check presenceVerifyNotExists(text) - Check absenceModalTester
OpenByAction(action) - Open modalCloseByAction(action) - Close modalVerifyVisible() - Check visibilityVerifyHidden() - Check hiddenFillForm(fields ...Field) - Fill modal formWaitForOpen(timeout) - Wait for modalWaitForClose(timeout) - Wait for closeWait Utilities
WaitFor(condition, timeout) - Wait for JavaScript condition to be trueWaitForText(selector, text, timeout) - Wait for element text to contain substringWaitForCount(selector, count, timeout) - Wait for specific element countWaitForWebSocketReady(timeout) - Wait for WebSocket connection and initial syncSame as LiveTemplate project.
Contributions welcome! Please ensure:
go fmt