#!/usr/bin/env bash set -euo pipefail # E2E Alpha Preview Test Suite for HyperFrames # Tests the full composition lifecycle: init → preview → render → validate SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" TEST_DIR="/tmp/hyperframes-e2e-$(date +%Y%m%d%H%M%S)" LOG_FILE="${TEST_DIR}/test-results.log" ISSUES_FILE="${TEST_DIR}/issues.log" PASS=0 FAIL=0 WARN=0 mkdir -p "$TEST_DIR" log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; } pass() { PASS=$((PASS + 1)); log " PASS: $*"; } fail() { FAIL=$((FAIL + 1)); log " FAIL: $*"; echo "ISSUE: $*" >> "$ISSUES_FILE"; } warn() { WARN=$((WARN + 1)); log " WARN: $*"; echo "WARNING: $*" >> "$ISSUES_FILE"; } section() { log ""; log "=== $* ==="; } # ── 1. CLI Version & Help ──────────────────────────────────────────────────── section "CLI basics" if hyperframes --version > /dev/null 2>&1; then VERSION=$(hyperframes --version) pass "CLI version: $VERSION" else fail "CLI --version fails" fi if hyperframes --help > /dev/null 2>&1; then pass "CLI --help works" else fail "CLI --help fails" fi for cmd in render preview init validate info; do if hyperframes $cmd --help > /dev/null 2>&1; then pass "CLI '$cmd --help' works" else fail "CLI '$cmd --help' fails" fi done # ── 2. Init & Validate compositions ───────────────────────────────────────── section "Init compositions" EXAMPLES=(warm-grain kinetic-type swiss-grid product-promo vignelli nyt-graph decision-tree) for example in "${EXAMPLES[@]}"; do PROJ_DIR="${TEST_DIR}/init-${example}" if hyperframes init "$PROJ_DIR" --example "$example" --non-interactive --skip-skills 2>&1 | tail -3 >> "$LOG_FILE"; then if [ -f "$PROJ_DIR/index.html" ]; then pass "init --example $example created project with index.html" else fail "init --example $example: missing index.html" fi else fail "init --example $example failed" fi done section "Validate compositions" for example in "${EXAMPLES[@]}"; do PROJ_DIR="${TEST_DIR}/init-${example}" if [ -d "$PROJ_DIR" ]; then VALIDATE_OUT=$(hyperframes validate "$PROJ_DIR" 2>&1) || true # Count only real errors (✗ lines), not contrast warnings or GSAP warnings ERROR_COUNT=$(echo "$VALIDATE_OUT" | grep -c '✗' || echo 0) if [ "$ERROR_COUNT" -gt 0 ]; then fail "validate $example: $ERROR_COUNT error(s)" echo "$VALIDATE_OUT" >> "$ISSUES_FILE" else CONTRAST_WARNINGS=$(echo "$VALIDATE_OUT" | grep -c 'contrast warning' || echo 0) if [ "$CONTRAST_WARNINGS" -gt 0 ]; then warn "validate $example: contrast warnings only" else pass "validate $example: clean" fi fi fi done # ── 3. Render compositions ────────────────────────────────────────────────── section "Render compositions (MP4)" RENDER_EXAMPLES=(warm-grain kinetic-type swiss-grid) for example in "${RENDER_EXAMPLES[@]}"; do PROJ_DIR="${TEST_DIR}/init-${example}" OUTPUT="${TEST_DIR}/render-${example}.mp4" if [ -d "$PROJ_DIR" ]; then log " Rendering $example..." RENDER_START=$(date +%s) if hyperframes render "$PROJ_DIR" --output "$OUTPUT" --quality draft --fps 24 2>&1 | tee -a "$LOG_FILE"; then RENDER_END=$(date +%s) RENDER_TIME=$((RENDER_END - RENDER_START)) if [ -f "$OUTPUT" ]; then SIZE=$(stat --printf="%s" "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT" 2>/dev/null || echo 0) if [ "$SIZE" -gt 1000 ]; then pass "render $example: ${SIZE} bytes in ${RENDER_TIME}s" # Validate with ffprobe if command -v ffprobe > /dev/null 2>&1; then DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$OUTPUT" 2>/dev/null || echo "0") VCODEC=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$OUTPUT" 2>/dev/null || echo "unknown") RESOLUTION=$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 "$OUTPUT" 2>/dev/null || echo "unknown") log " Duration: ${DURATION}s, Codec: $VCODEC, Resolution: $RESOLUTION" if [ "$(echo "$DURATION > 0.5" | bc -l 2>/dev/null || echo 0)" = "1" ]; then pass "render $example: valid duration ${DURATION}s" else fail "render $example: duration too short (${DURATION}s)" fi fi else fail "render $example: output too small (${SIZE} bytes)" fi else fail "render $example: output file missing" fi else fail "render $example: command failed" fi fi done # ── 4. Render format variants ──────────────────────────────────────────────── section "Render format variants" PROJ_DIR="${TEST_DIR}/init-warm-grain" for format in webm mov; do OUTPUT="${TEST_DIR}/render-warm-grain.${format}" if [ -d "$PROJ_DIR" ]; then log " Rendering warm-grain as $format..." if hyperframes render "$PROJ_DIR" --output "$OUTPUT" --format "$format" --quality draft --fps 24 2>&1 | tee -a "$LOG_FILE"; then if [ -f "$OUTPUT" ]; then SIZE=$(stat --printf="%s" "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT" 2>/dev/null || echo 0) if [ "$SIZE" -gt 1000 ]; then pass "render warm-grain.$format: ${SIZE} bytes" else fail "render warm-grain.$format: output too small (${SIZE} bytes)" fi else fail "render warm-grain.$format: output file missing" fi else fail "render warm-grain.$format: command failed" fi fi done # ── 5. Sub-composition rendering ───────────────────────────────────────────── section "Sub-composition rendering" for example in "${EXAMPLES[@]}"; do PROJ_DIR="${TEST_DIR}/init-${example}" if [ -d "$PROJ_DIR/compositions" ]; then COMPS=$(find "$PROJ_DIR/compositions" -name '*.html' 2>/dev/null | head -3) if [ -n "$COMPS" ]; then for comp in $COMPS; do COMP_REL=$(echo "$comp" | sed "s|$PROJ_DIR/||") OUTPUT="${TEST_DIR}/render-${example}-sub-$(basename "$comp" .html).mp4" log " Rendering sub-comp: $example / $COMP_REL" if hyperframes render "$PROJ_DIR" -c "$COMP_REL" --output "$OUTPUT" --quality draft --fps 24 2>&1 | tee -a "$LOG_FILE"; then if [ -f "$OUTPUT" ]; then SIZE=$(stat --printf="%s" "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT" 2>/dev/null || echo 0) if [ "$SIZE" -gt 1000 ]; then pass "sub-comp $example/$COMP_REL: ${SIZE} bytes" else fail "sub-comp $example/$COMP_REL: output too small (${SIZE} bytes)" fi else fail "sub-comp $example/$COMP_REL: output file missing" fi else fail "sub-comp $example/$COMP_REL: render failed" fi done else log " (no sub-compositions in $example)" fi fi done # ── 6. Preview server health check ────────────────────────────────────────── section "Preview server health check" PROJ_DIR="${TEST_DIR}/init-warm-grain" PREVIEW_PORT=13099 PREVIEW_PID="" if [ -d "$PROJ_DIR" ]; then hyperframes preview "$PROJ_DIR" --port "$PREVIEW_PORT" & PREVIEW_PID=$! sleep 5 if kill -0 "$PREVIEW_PID" 2>/dev/null; then pass "preview server started (PID $PREVIEW_PID)" # Test API endpoints for endpoint in "/api/projects" "/api/runtime.js"; do STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PREVIEW_PORT}${endpoint}" 2>/dev/null || echo "000") if [ "$STATUS" = "200" ]; then pass "GET $endpoint: $STATUS" else fail "GET $endpoint: $STATUS (expected 200)" fi done # Test preview endpoint PROJECTS_JSON=$(curl -s "http://localhost:${PREVIEW_PORT}/api/projects" 2>/dev/null || echo "[]") if echo "$PROJECTS_JSON" | python3 -c "import sys,json; data=json.load(sys.stdin); assert len(data) > 0" 2>/dev/null; then pass "GET /api/projects returns project list" PROJECT_ID=$(echo "$PROJECTS_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])" 2>/dev/null || echo "") if [ -n "$PROJECT_ID" ]; then STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PREVIEW_PORT}/api/projects/${PROJECT_ID}/preview" 2>/dev/null || echo "000") if [ "$STATUS" = "200" ]; then pass "GET /api/projects/$PROJECT_ID/preview: $STATUS" else fail "GET /api/projects/$PROJECT_ID/preview: $STATUS" fi fi else fail "GET /api/projects returns empty or invalid JSON" fi kill "$PREVIEW_PID" 2>/dev/null || true wait "$PREVIEW_PID" 2>/dev/null || true else fail "preview server failed to start" fi fi # ── 7. Lint check on compositions ──────────────────────────────────────────── section "Lint compositions" for example in "${EXAMPLES[@]}"; do PROJ_DIR="${TEST_DIR}/init-${example}" if [ -d "$PROJ_DIR" ]; then LINT_OUT=$(hyperframes lint "$PROJ_DIR" 2>&1) || true LINT_EXIT=$? if [ $LINT_EXIT -eq 0 ]; then pass "lint $example: clean" else # Lint warnings are ok, errors are not if echo "$LINT_OUT" | grep -qi "error"; then fail "lint $example: errors found" echo "$LINT_OUT" >> "$ISSUES_FILE" else warn "lint $example: warnings" fi fi fi done # ── 8. Info command ────────────────────────────────────────────────────────── section "Info command" for example in warm-grain kinetic-type; do PROJ_DIR="${TEST_DIR}/init-${example}" if [ -d "$PROJ_DIR" ]; then if hyperframes info "$PROJ_DIR" 2>&1 | tee -a "$LOG_FILE" | grep -qi "duration\|fps\|resolution"; then pass "info $example: shows metadata" else fail "info $example: missing metadata in output" fi fi done # ── 9. Parallel render (if supported) ─────────────────────────────────────── section "Parallel render" PROJ_DIR="${TEST_DIR}/init-warm-grain" OUTPUT="${TEST_DIR}/render-parallel.mp4" if [ -d "$PROJ_DIR" ]; then log " Rendering warm-grain with --workers 2..." if hyperframes render "$PROJ_DIR" --output "$OUTPUT" --workers 2 --quality draft --fps 24 2>&1 | tee -a "$LOG_FILE"; then if [ -f "$OUTPUT" ]; then SIZE=$(stat --printf="%s" "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT" 2>/dev/null || echo 0) if [ "$SIZE" -gt 1000 ]; then pass "parallel render (2 workers): ${SIZE} bytes" else fail "parallel render: output too small (${SIZE} bytes)" fi else fail "parallel render: output file missing" fi else fail "parallel render: command failed" fi fi # ── 10. PNG sequence render ───────────────────────────────────────────────── section "PNG sequence render" PROJ_DIR="${TEST_DIR}/init-warm-grain" OUTPUT_DIR="${TEST_DIR}/render-png-seq" if [ -d "$PROJ_DIR" ]; then if hyperframes render "$PROJ_DIR" --output "$OUTPUT_DIR" --format png-sequence --quality draft --fps 24 2>&1 | tee -a "$LOG_FILE"; then PNG_COUNT=$(find "$OUTPUT_DIR" -name '*.png' 2>/dev/null | wc -l) if [ "$PNG_COUNT" -gt 5 ]; then pass "png-sequence render: $PNG_COUNT frames" else fail "png-sequence render: only $PNG_COUNT frames" fi else fail "png-sequence render: command failed" fi fi # ── 11. Resolution variants ───────────────────────────────────────────────── section "Resolution variants" PROJ_DIR="${TEST_DIR}/init-warm-grain" # Only test resolutions matching the composition's aspect ratio. # warm-grain is landscape (1920x1080) — portrait would correctly fail. for res in landscape landscape-4k; do OUTPUT="${TEST_DIR}/render-${res}.mp4" if [ -d "$PROJ_DIR" ]; then log " Rendering warm-grain at $res resolution..." if hyperframes render "$PROJ_DIR" --output "$OUTPUT" --resolution "$res" --quality draft --fps 24 2>&1 | tee -a "$LOG_FILE"; then if [ -f "$OUTPUT" ]; then ACTUAL_RES=$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 "$OUTPUT" 2>/dev/null || echo "unknown") pass "render resolution=$res: $ACTUAL_RES" else fail "render resolution=$res: output file missing" fi else fail "render resolution=$res: command failed" fi fi done # ── Summary ────────────────────────────────────────────────────────────────── section "Summary" log "PASS: $PASS | FAIL: $FAIL | WARN: $WARN" log "Test dir: $TEST_DIR" log "Log: $LOG_FILE" if [ -f "$ISSUES_FILE" ]; then log "Issues: $ISSUES_FILE" log "" log "Issues found:" cat "$ISSUES_FILE" | tee -a "$LOG_FILE" fi # Cleanup preview server if still running if [ -n "$PREVIEW_PID" ] && kill -0 "$PREVIEW_PID" 2>/dev/null; then kill "$PREVIEW_PID" 2>/dev/null || true fi echo "" echo "EXIT_CODE: $FAIL" exit 0